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