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