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