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