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    /**
129     * Constructs a new {@code UploadAction}.
130     */
131    public UploadAction() {
132        super(tr("Upload data"), "upload", tr("Upload all changes in the active data layer to the OSM server"),
133                Shortcut.registerShortcut("file:upload", tr("File: {0}", tr("Upload data")), KeyEvent.VK_UP, Shortcut.CTRL_SHIFT), true);
134        putValue("help", ht("/Action/Upload"));
135    }
136
137    /**
138     * Refreshes the enabled state
139     *
140     */
141    @Override
142    protected void updateEnabledState() {
143        setEnabled(getLayerManager().getEditLayer() != null);
144    }
145
146    public static boolean checkPreUploadConditions(AbstractModifiableLayer layer) {
147        return checkPreUploadConditions(layer,
148                layer instanceof OsmDataLayer ? new APIDataSet(((OsmDataLayer) layer).data) : null);
149    }
150
151    protected static void alertUnresolvedConflicts(OsmDataLayer layer) {
152        HelpAwareOptionPane.showOptionDialog(
153                Main.parent,
154                tr("<html>The data to be uploaded participates in unresolved conflicts of layer ''{0}''.<br>"
155                        + "You have to resolve them first.</html>", layer.getName()
156                ),
157                tr("Warning"),
158                JOptionPane.WARNING_MESSAGE,
159                ht("/Action/Upload#PrimitivesParticipateInConflicts")
160        );
161    }
162
163    /**
164     * Warn user about discouraged upload, propose to cancel operation.
165     * @param layer incriminated layer
166     * @return true if the user wants to cancel, false if they want to continue
167     */
168    public static boolean warnUploadDiscouraged(AbstractModifiableLayer layer) {
169        return GuiHelper.warnUser(tr("Upload discouraged"),
170                "<html>" +
171                tr("You are about to upload data from the layer ''{0}''.<br /><br />"+
172                    "Sending data from this layer is <b>strongly discouraged</b>. If you continue,<br />"+
173                    "it may require you subsequently have to revert your changes, or force other contributors to.<br /><br />"+
174                    "Are you sure you want to continue?", layer.getName())+
175                "</html>",
176                ImageProvider.get("upload"), tr("Ignore this hint and upload anyway"));
177    }
178
179    /**
180     * Check whether the preconditions are met to upload data in <code>apiData</code>.
181     * Makes sure upload is allowed, primitives in <code>apiData</code> don't participate in conflicts and
182     * runs the installed {@link UploadHook}s.
183     *
184     * @param layer the source layer of the data to be uploaded
185     * @param apiData the data to be uploaded
186     * @return true, if the preconditions are met; false, otherwise
187     */
188    public static boolean checkPreUploadConditions(AbstractModifiableLayer layer, APIDataSet apiData) {
189        if (layer.isUploadDiscouraged() && warnUploadDiscouraged(layer)) {
190            return false;
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        dialog.setChangesetTags(layer.data);
234        dialog.setUploadedPrimitives(apiData);
235        dialog.setVisible(true);
236        dialog.rememberUserInput();
237        if (dialog.isCanceled())
238            return;
239
240        for (UploadHook hook : lateUploadHooks) {
241            if (!hook.checkUpload(apiData))
242                return;
243        }
244
245        Main.worker.execute(
246                new UploadPrimitivesTask(
247                        UploadDialog.getUploadDialog().getUploadStrategySpecification(),
248                        layer,
249                        apiData,
250                        UploadDialog.getUploadDialog().getChangeset()
251                )
252        );
253    }
254
255    @Override
256    public void actionPerformed(ActionEvent e) {
257        if (!isEnabled())
258            return;
259        if (Main.map == null) {
260            JOptionPane.showMessageDialog(
261                    Main.parent,
262                    tr("Nothing to upload. Get some data first."),
263                    tr("Warning"),
264                    JOptionPane.WARNING_MESSAGE
265            );
266            return;
267        }
268        APIDataSet apiData = new APIDataSet(Main.getLayerManager().getEditDataSet());
269        uploadData(Main.getLayerManager().getEditLayer(), apiData);
270    }
271}