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