001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.event.ActionEvent;
008import java.awt.event.KeyEvent;
009import java.util.HashMap;
010import java.util.LinkedList;
011import java.util.List;
012
013import javax.swing.JOptionPane;
014import javax.swing.SwingUtilities;
015
016import org.openstreetmap.josm.Main;
017import org.openstreetmap.josm.actions.upload.ApiPreconditionCheckerHook;
018import org.openstreetmap.josm.actions.upload.DiscardTagsHook;
019import org.openstreetmap.josm.actions.upload.FixDataHook;
020import org.openstreetmap.josm.actions.upload.RelationUploadOrderHook;
021import org.openstreetmap.josm.actions.upload.UploadHook;
022import org.openstreetmap.josm.actions.upload.ValidateUploadHook;
023import org.openstreetmap.josm.data.APIDataSet;
024import org.openstreetmap.josm.data.conflict.ConflictCollection;
025import org.openstreetmap.josm.gui.HelpAwareOptionPane;
026import org.openstreetmap.josm.gui.help.HelpUtil;
027import org.openstreetmap.josm.gui.io.UploadDialog;
028import org.openstreetmap.josm.gui.io.UploadPrimitivesTask;
029import org.openstreetmap.josm.gui.layer.OsmDataLayer;
030import org.openstreetmap.josm.gui.util.GuiHelper;
031import org.openstreetmap.josm.tools.ImageProvider;
032import org.openstreetmap.josm.tools.Shortcut;
033
034/**
035 * Action that opens a connection to the osm server and uploads all changes.
036 *
037 * An dialog is displayed asking the user to specify a rectangle to grab.
038 * The url and account settings from the preferences are used.
039 *
040 * If the upload fails this action offers various options to resolve conflicts.
041 *
042 * @author imi
043 */
044public class UploadAction extends JosmAction{
045    /**
046     * The list of upload hooks. These hooks will be called one after the other
047     * when the user wants to upload data. Plugins can insert their own hooks here
048     * if they want to be able to veto an upload.
049     *
050     * Be default, the standard upload dialog is the only element in the list.
051     * Plugins should normally insert their code before that, so that the upload
052     * dialog is the last thing shown before upload really starts; on occasion
053     * however, a plugin might also want to insert something after that.
054     */
055    private static final List<UploadHook> uploadHooks = new LinkedList<>();
056    private static final List<UploadHook> lateUploadHooks = new LinkedList<>();
057    static {
058        /**
059         * Calls validator before upload.
060         */
061        uploadHooks.add(new ValidateUploadHook());
062
063        /**
064         * Fixes database errors
065         */
066        uploadHooks.add(new FixDataHook());
067
068        /**
069         * Checks server capabilities before upload.
070         */
071        uploadHooks.add(new ApiPreconditionCheckerHook());
072
073        /**
074         * Adjusts the upload order of new relations
075         */
076        uploadHooks.add(new RelationUploadOrderHook());
077
078        /**
079         * Removes discardable tags like created_by on modified objects
080         */
081        lateUploadHooks.add(new DiscardTagsHook());
082    }
083
084    /**
085     * Registers an upload hook. Adds the hook at the first position of the upload hooks.
086     *
087     * @param hook the upload hook. Ignored if null.
088     */
089    public static void registerUploadHook(UploadHook hook) {
090        registerUploadHook(hook, false);
091    }
092
093    /**
094     * Registers an upload hook. Adds the hook at the first position of the upload hooks.
095     *
096     * @param hook the upload hook. Ignored if null.
097     * @param late true, if the hook should be executed after the upload dialog
098     * has been confirmed. Late upload hooks should in general succeed and not
099     * abort the upload.
100     */
101    public static void registerUploadHook(UploadHook hook, boolean late) {
102        if(hook == null) return;
103        if (late) {
104            if (!lateUploadHooks.contains(hook)) {
105                lateUploadHooks.add(0, hook);
106            }
107        } else {
108            if (!uploadHooks.contains(hook)) {
109                uploadHooks.add(0, hook);
110            }
111        }
112    }
113
114    /**
115     * Unregisters an upload hook. Removes the hook from the list of upload hooks.
116     *
117     * @param hook the upload hook. Ignored if null.
118     */
119    public static void unregisterUploadHook(UploadHook hook) {
120        if(hook == null) return;
121        if (uploadHooks.contains(hook)) {
122            uploadHooks.remove(hook);
123        }
124        if (lateUploadHooks.contains(hook)) {
125            lateUploadHooks.remove(hook);
126        }
127    }
128
129    public UploadAction() {
130        super(tr("Upload data"), "upload", tr("Upload all changes in the active data layer to the OSM server"),
131                Shortcut.registerShortcut("file:upload", tr("File: {0}", tr("Upload data")), KeyEvent.VK_UP, Shortcut.CTRL_SHIFT), true);
132        putValue("help", ht("/Action/Upload"));
133    }
134
135    /**
136     * Refreshes the enabled state
137     *
138     */
139    @Override
140    protected void updateEnabledState() {
141        setEnabled(getEditLayer() != null);
142    }
143
144    public boolean checkPreUploadConditions(OsmDataLayer layer) {
145        return checkPreUploadConditions(layer, new APIDataSet(layer.data));
146    }
147
148    protected static void alertUnresolvedConflicts(OsmDataLayer layer) {
149        HelpAwareOptionPane.showOptionDialog(
150                Main.parent,
151                tr("<html>The data to be uploaded participates in unresolved conflicts of layer ''{0}''.<br>"
152                        + "You have to resolve them first.</html>", layer.getName()
153                ),
154                tr("Warning"),
155                JOptionPane.WARNING_MESSAGE,
156                HelpUtil.ht("/Action/Upload#PrimitivesParticipateInConflicts")
157        );
158    }
159
160    /**
161     * returns true if the user wants to cancel, false if they
162     * want to continue
163     */
164    public static boolean warnUploadDiscouraged(OsmDataLayer layer) {
165        return GuiHelper.warnUser(tr("Upload discouraged"),
166                "<html>" +
167                tr("You are about to upload data from the layer ''{0}''.<br /><br />"+
168                    "Sending data from this layer is <b>strongly discouraged</b>. If you continue,<br />"+
169                    "it may require you subsequently have to revert your changes, or force other contributors to.<br /><br />"+
170                    "Are you sure you want to continue?", layer.getName())+
171                "</html>",
172                ImageProvider.get("upload"), tr("Ignore this hint and upload anyway"));
173    }
174
175    /**
176     * Check whether the preconditions are met to upload data in <code>apiData</code>.
177     * Makes sure upload is allowed, primitives in <code>apiData</code> don't participate in conflicts and
178     * runs the installed {@link UploadHook}s.
179     *
180     * @param layer the source layer of the data to be uploaded
181     * @param apiData the data to be uploaded
182     * @return true, if the preconditions are met; false, otherwise
183     */
184    public boolean checkPreUploadConditions(OsmDataLayer layer, APIDataSet apiData) {
185        if (layer.isUploadDiscouraged()) {
186            if (warnUploadDiscouraged(layer)) {
187                return false;
188            }
189        }
190        ConflictCollection conflicts = layer.getConflicts();
191        if (apiData.participatesInConflict(conflicts)) {
192            alertUnresolvedConflicts(layer);
193            return false;
194        }
195        // Call all upload hooks in sequence.
196        // FIXME: this should become an asynchronous task
197        //
198        for (UploadHook hook : uploadHooks) {
199            if (!hook.checkUpload(apiData))
200                return false;
201        }
202
203        return true;
204    }
205
206    /**
207     * Uploads data to the OSM API.
208     *
209     * @param layer the source layer for the data to upload
210     * @param apiData the primitives to be added, updated, or deleted
211     */
212    public void uploadData(final OsmDataLayer layer, APIDataSet apiData) {
213        if (apiData.isEmpty()) {
214            JOptionPane.showMessageDialog(
215                    Main.parent,
216                    tr("No changes to upload."),
217                    tr("Warning"),
218                    JOptionPane.INFORMATION_MESSAGE
219            );
220            return;
221        }
222        if (!checkPreUploadConditions(layer, apiData))
223            return;
224
225        final UploadDialog dialog = UploadDialog.getUploadDialog();
226        // If we simply set the changeset comment here, it would be
227        // overridden by subsequent events in EDT that are caused by
228        // dialog creation. The current solution is to queue this operation
229        // after these events.
230        // TODO: find better way to initialize the comment field
231        SwingUtilities.invokeLater(new Runnable() {
232            @Override
233            public void run() {
234                final HashMap<String, String> tags = new HashMap<>(layer.data.getChangeSetTags());
235                if (!tags.containsKey("source")) {
236                    tags.put("source", dialog.getLastChangesetSourceFromHistory());
237                }
238                if (!tags.containsKey("comment")) {
239                    tags.put("comment", dialog.getLastChangesetCommentFromHistory());
240                }
241                dialog.setDefaultChangesetTags(tags);
242            }
243        });
244        dialog.setUploadedPrimitives(apiData);
245        dialog.setVisible(true);
246        if (dialog.isCanceled())
247            return;
248        dialog.rememberUserInput();
249
250        for (UploadHook hook : lateUploadHooks) {
251            if (!hook.checkUpload(apiData))
252                return;
253        }
254
255        Main.worker.execute(
256                new UploadPrimitivesTask(
257                        UploadDialog.getUploadDialog().getUploadStrategySpecification(),
258                        layer,
259                        apiData,
260                        UploadDialog.getUploadDialog().getChangeset()
261                )
262        );
263    }
264
265    @Override
266    public void actionPerformed(ActionEvent e) {
267        if (!isEnabled())
268            return;
269        if (Main.map == null) {
270            JOptionPane.showMessageDialog(
271                    Main.parent,
272                    tr("Nothing to upload. Get some data first."),
273                    tr("Warning"),
274                    JOptionPane.WARNING_MESSAGE
275            );
276            return;
277        }
278        APIDataSet apiData = new APIDataSet(Main.main.getCurrentDataSet());
279        uploadData(Main.main.getEditLayer(), apiData);
280    }
281}