source: josm/trunk/src/org/openstreetmap/josm/actions/UploadAction.java

Last change on this file was 18467, checked in by taylor.smock, 8 months ago

Fix #20025, #22080: Notify users when changeset tags are programmatically modified

This only works for non-late upload hooks.

  • Property svn:eol-style set to native
File size: 11.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.util.HashMap;
10import java.util.LinkedList;
11import java.util.List;
12import java.util.Map;
13import java.util.Optional;
14
15import javax.swing.JOptionPane;
16
17import org.openstreetmap.josm.actions.upload.ApiPreconditionCheckerHook;
18import org.openstreetmap.josm.actions.upload.DiscardTagsHook;
19import org.openstreetmap.josm.actions.upload.FixDataHook;
20import org.openstreetmap.josm.actions.upload.RelationUploadOrderHook;
21import org.openstreetmap.josm.actions.upload.UploadHook;
22import org.openstreetmap.josm.actions.upload.ValidateUploadHook;
23import org.openstreetmap.josm.data.APIDataSet;
24import org.openstreetmap.josm.data.conflict.ConflictCollection;
25import org.openstreetmap.josm.data.osm.Changeset;
26import org.openstreetmap.josm.gui.HelpAwareOptionPane;
27import org.openstreetmap.josm.gui.MainApplication;
28import org.openstreetmap.josm.gui.Notification;
29import org.openstreetmap.josm.gui.io.AsynchronousUploadPrimitivesTask;
30import org.openstreetmap.josm.gui.io.UploadDialog;
31import org.openstreetmap.josm.gui.io.UploadPrimitivesTask;
32import org.openstreetmap.josm.gui.layer.AbstractModifiableLayer;
33import org.openstreetmap.josm.gui.layer.OsmDataLayer;
34import org.openstreetmap.josm.gui.util.GuiHelper;
35import org.openstreetmap.josm.io.ChangesetUpdater;
36import org.openstreetmap.josm.io.UploadStrategySpecification;
37import org.openstreetmap.josm.spi.preferences.Config;
38import org.openstreetmap.josm.tools.ImageProvider;
39import org.openstreetmap.josm.tools.Logging;
40import org.openstreetmap.josm.tools.Shortcut;
41import org.openstreetmap.josm.tools.Utils;
42
43/**
44 * Action that opens a connection to the osm server and uploads all changes.
45 *
46 * A dialog is displayed asking the user to specify a rectangle to grab.
47 * The url and account settings from the preferences are used.
48 *
49 * If the upload fails this action offers various options to resolve conflicts.
50 *
51 * @author imi
52 */
53public class UploadAction extends AbstractUploadAction {
54    /**
55     * The list of upload hooks. These hooks will be called one after the other
56     * when the user wants to upload data. Plugins can insert their own hooks here
57     * if they want to be able to veto an upload.
58     *
59     * Be default, the standard upload dialog is the only element in the list.
60     * Plugins should normally insert their code before that, so that the upload
61     * dialog is the last thing shown before upload really starts; on occasion
62     * however, a plugin might also want to insert something after that.
63     */
64    private static final List<UploadHook> UPLOAD_HOOKS = new LinkedList<>();
65    private static final List<UploadHook> LATE_UPLOAD_HOOKS = new LinkedList<>();
66
67    private static final String IS_ASYNC_UPLOAD_ENABLED = "asynchronous.upload";
68
69    static {
70        /**
71         * Calls validator before upload.
72         */
73        UPLOAD_HOOKS.add(new ValidateUploadHook());
74
75        /**
76         * Fixes database errors
77         */
78        UPLOAD_HOOKS.add(new FixDataHook());
79
80        /**
81         * Checks server capabilities before upload.
82         */
83        UPLOAD_HOOKS.add(new ApiPreconditionCheckerHook());
84
85        /**
86         * Adjusts the upload order of new relations
87         */
88        UPLOAD_HOOKS.add(new RelationUploadOrderHook());
89
90        /**
91         * Removes discardable tags like created_by on modified objects
92         */
93        LATE_UPLOAD_HOOKS.add(new DiscardTagsHook());
94    }
95
96    /**
97     * Registers an upload hook. Adds the hook at the first position of the upload hooks.
98     *
99     * @param hook the upload hook. Ignored if null.
100     */
101    public static void registerUploadHook(UploadHook hook) {
102        registerUploadHook(hook, false);
103    }
104
105    /**
106     * Registers an upload hook. Adds the hook at the first position of the upload hooks.
107     *
108     * @param hook the upload hook. Ignored if null.
109     * @param late true, if the hook should be executed after the upload dialog
110     * has been confirmed. Late upload hooks should in general succeed and not
111     * abort the upload.
112     */
113    public static void registerUploadHook(UploadHook hook, boolean late) {
114        if (hook == null) return;
115        if (late) {
116            if (!LATE_UPLOAD_HOOKS.contains(hook)) {
117                LATE_UPLOAD_HOOKS.add(0, hook);
118            }
119        } else {
120            if (!UPLOAD_HOOKS.contains(hook)) {
121                UPLOAD_HOOKS.add(0, hook);
122            }
123        }
124    }
125
126    /**
127     * Unregisters an upload hook. Removes the hook from the list of upload hooks.
128     *
129     * @param hook the upload hook. Ignored if null.
130     */
131    public static void unregisterUploadHook(UploadHook hook) {
132        if (hook == null) return;
133        UPLOAD_HOOKS.remove(hook);
134        LATE_UPLOAD_HOOKS.remove(hook);
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        setHelpId(ht("/Action/Upload"));
144    }
145
146    @Override
147    protected boolean listenToSelectionChange() {
148        return false;
149    }
150
151    @Override
152    protected void updateEnabledState() {
153        OsmDataLayer editLayer = getLayerManager().getEditLayer();
154        setEnabled(editLayer != null && editLayer.requiresUploadToServer());
155    }
156
157    /**
158     * Check whether the preconditions are met to upload data from a given layer, if applicable.
159     * @param layer layer to check
160     * @return {@code true} if the preconditions are met, or not applicable
161     * @see #checkPreUploadConditions(AbstractModifiableLayer, APIDataSet)
162     */
163    public static boolean checkPreUploadConditions(AbstractModifiableLayer layer) {
164        return checkPreUploadConditions(layer,
165                layer instanceof OsmDataLayer ? new APIDataSet(((OsmDataLayer) layer).getDataSet()) : null);
166    }
167
168    protected static void alertUnresolvedConflicts(OsmDataLayer layer) {
169        HelpAwareOptionPane.showOptionDialog(
170                MainApplication.getMainFrame(),
171                tr("<html>The data to be uploaded participates in unresolved conflicts of layer ''{0}''.<br>"
172                        + "You have to resolve them first.</html>", Utils.escapeReservedCharactersHTML(layer.getName())
173                ),
174                tr("Warning"),
175                JOptionPane.WARNING_MESSAGE,
176                ht("/Action/Upload#PrimitivesParticipateInConflicts")
177        );
178    }
179
180    /**
181     * Warn user about discouraged upload, propose to cancel operation.
182     * @param layer incriminated layer
183     * @return true if the user wants to cancel, false if they want to continue
184     */
185    public static boolean warnUploadDiscouraged(AbstractModifiableLayer layer) {
186        return GuiHelper.warnUser(tr("Upload discouraged"),
187                "<html>" +
188                tr("You are about to upload data from the layer ''{0}''.<br /><br />"+
189                    "Sending data from this layer is <b>strongly discouraged</b>. If you continue,<br />"+
190                    "it may require you subsequently have to revert your changes, or force other contributors to.<br /><br />"+
191                    "Are you sure you want to continue?", Utils.escapeReservedCharactersHTML(layer.getName()))+
192                "</html>",
193                ImageProvider.get("upload"), tr("Ignore this hint and upload anyway"));
194    }
195
196    /**
197     * Check whether the preconditions are met to upload data in <code>apiData</code>.
198     * Makes sure upload is allowed, primitives in <code>apiData</code> don't participate in conflicts and
199     * runs the installed {@link UploadHook}s.
200     *
201     * @param layer the source layer of the data to be uploaded
202     * @param apiData the data to be uploaded
203     * @return true, if the preconditions are met; false, otherwise
204     */
205    public static boolean checkPreUploadConditions(AbstractModifiableLayer layer, APIDataSet apiData) {
206        if (layer.isUploadDiscouraged() && warnUploadDiscouraged(layer)) {
207            return false;
208        }
209        if (layer instanceof OsmDataLayer) {
210            OsmDataLayer osmLayer = (OsmDataLayer) layer;
211            ConflictCollection conflicts = osmLayer.getConflicts();
212            if (apiData.participatesInConflict(conflicts)) {
213                alertUnresolvedConflicts(osmLayer);
214                return false;
215            }
216        }
217        // Call all upload hooks in sequence.
218        // FIXME: this should become an asynchronous task
219        //
220        if (apiData != null) {
221            return UPLOAD_HOOKS.stream().allMatch(hook -> hook.checkUpload(apiData));
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.setUploadedPrimitives(apiData);
245        dialog.initLifeCycle(layer.getDataSet());
246        Map<String, String> changesetTags = dialog.getChangeset().getKeys();
247        Map<String, String> originalChangesetTags = new HashMap<>(changesetTags);
248        for (UploadHook hook : UPLOAD_HOOKS) {
249            hook.modifyChangesetTags(changesetTags);
250        }
251        dialog.getModel().putAll(changesetTags);
252        if (!originalChangesetTags.equals(changesetTags)) {
253            dialog.setChangesetTagsModifiedProgramatically();
254        }
255        dialog.setVisible(true);
256        dialog.rememberUserInput();
257        if (dialog.isCanceled()) {
258            dialog.clean();
259            return;
260        }
261
262        for (UploadHook hook : LATE_UPLOAD_HOOKS) {
263            if (!hook.checkUpload(apiData)) {
264                dialog.clean();
265                return;
266            }
267        }
268
269        // Any hooks want to change the changeset tags?
270        Changeset cs = dialog.getChangeset();
271        changesetTags = cs.getKeys();
272        for (UploadHook hook : LATE_UPLOAD_HOOKS) {
273            hook.modifyChangesetTags(changesetTags);
274        }
275
276        UploadStrategySpecification uploadStrategySpecification = dialog.getUploadStrategySpecification();
277        Logging.info("Starting upload with tags {0}", changesetTags);
278        Logging.info(uploadStrategySpecification.toString());
279        Logging.info(cs.toString());
280        dialog.clean();
281
282        if (Config.getPref().getBoolean(IS_ASYNC_UPLOAD_ENABLED, true)) {
283            Optional<AsynchronousUploadPrimitivesTask> asyncUploadTask = AsynchronousUploadPrimitivesTask.createAsynchronousUploadTask(
284                    uploadStrategySpecification, layer, apiData, cs);
285
286            if (asyncUploadTask.isPresent()) {
287                MainApplication.worker.execute(asyncUploadTask.get());
288            }
289        } else {
290            MainApplication.worker.execute(new UploadPrimitivesTask(uploadStrategySpecification, layer, apiData, cs));
291        }
292    }
293
294    @Override
295    public void actionPerformed(ActionEvent e) {
296        if (!isEnabled())
297            return;
298        if (MainApplication.getMap() == null) {
299            new Notification(tr("Nothing to upload. Get some data first.")).show();
300            return;
301        }
302        APIDataSet apiData = new APIDataSet(getLayerManager().getEditDataSet());
303        uploadData(getLayerManager().getEditLayer(), apiData);
304    }
305}
Note: See TracBrowser for help on using the repository browser.