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