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