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;
013import javax.swing.SwingUtilities;
014
015import org.openstreetmap.josm.Main;
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.gui.HelpAwareOptionPane;
025import org.openstreetmap.josm.gui.help.HelpUtil;
026import org.openstreetmap.josm.gui.io.UploadDialog;
027import org.openstreetmap.josm.gui.io.UploadPrimitivesTask;
028import org.openstreetmap.josm.gui.layer.OsmDataLayer;
029import org.openstreetmap.josm.gui.util.GuiHelper;
030import org.openstreetmap.josm.tools.ImageProvider;
031import org.openstreetmap.josm.tools.Shortcut;
032
033/**
034 * Action that opens a connection to the osm server and uploads all changes.
035 *
036 * An dialog is displayed asking the user to specify a rectangle to grab.
037 * The url and account settings from the preferences are used.
038 *
039 * If the upload fails this action offers various options to resolve conflicts.
040 *
041 * @author imi
042 */
043public class UploadAction extends JosmAction{
044    /**
045     * The list of upload hooks. These hooks will be called one after the other
046     * when the user wants to upload data. Plugins can insert their own hooks here
047     * if they want to be able to veto an upload.
048     *
049     * Be default, the standard upload dialog is the only element in the list.
050     * Plugins should normally insert their code before that, so that the upload
051     * dialog is the last thing shown before upload really starts; on occasion
052     * however, a plugin might also want to insert something after that.
053     */
054    private static final List<UploadHook> uploadHooks = new LinkedList<UploadHook>();
055    private static final List<UploadHook> lateUploadHooks = new LinkedList<UploadHook>();
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 boolean checkPreUploadConditions(OsmDataLayer layer) {
144        return checkPreUploadConditions(layer, new APIDataSet(layer.data));
145    }
146
147    protected static void alertUnresolvedConflicts(OsmDataLayer layer) {
148        HelpAwareOptionPane.showOptionDialog(
149                Main.parent,
150                tr("<html>The data to be uploaded participates in unresolved conflicts of layer ''{0}''.<br>"
151                        + "You have to resolve them first.</html>", layer.getName()
152                ),
153                tr("Warning"),
154                JOptionPane.WARNING_MESSAGE,
155                HelpUtil.ht("/Action/Upload#PrimitivesParticipateInConflicts")
156        );
157    }
158
159    /**
160     * returns true if the user wants to cancel, false if they
161     * want to continue
162     */
163    public static boolean warnUploadDiscouraged(OsmDataLayer layer) {
164        return GuiHelper.warnUser(tr("Upload discouraged"),
165                "<html>" +
166                tr("You are about to upload data from the layer ''{0}''.<br /><br />"+
167                    "Sending data from this layer is <b>strongly discouraged</b>. If you continue,<br />"+
168                    "it may require you subsequently have to revert your changes, or force other contributors to.<br /><br />"+
169                    "Are you sure you want to continue?", layer.getName())+
170                "</html>",
171                ImageProvider.get("upload"), tr("Ignore this hint and upload anyway"));
172    }
173
174    /**
175     * Check whether the preconditions are met to upload data in <code>apiData</code>.
176     * Makes sure upload is allowed, primitives in <code>apiData</code> don't participate in conflicts and
177     * runs the installed {@link UploadHook}s.
178     *
179     * @param layer the source layer of the data to be uploaded
180     * @param apiData the data to be uploaded
181     * @return true, if the preconditions are met; false, otherwise
182     */
183    public boolean checkPreUploadConditions(OsmDataLayer layer, APIDataSet apiData) {
184        if (layer.isUploadDiscouraged()) {
185            if (warnUploadDiscouraged(layer)) {
186                return false;
187            }
188        }
189        ConflictCollection conflicts = layer.getConflicts();
190        if (apiData.participatesInConflict(conflicts)) {
191            alertUnresolvedConflicts(layer);
192            return false;
193        }
194        // Call all upload hooks in sequence.
195        // FIXME: this should become an asynchronous task
196        //
197        for (UploadHook hook : uploadHooks) {
198            if (!hook.checkUpload(apiData))
199                return false;
200        }
201
202        return true;
203    }
204
205    /**
206     * Uploads data to the OSM API.
207     *
208     * @param layer the source layer for the data to upload
209     * @param apiData the primitives to be added, updated, or deleted
210     */
211    public void uploadData(final OsmDataLayer layer, APIDataSet apiData) {
212        if (apiData.isEmpty()) {
213            JOptionPane.showMessageDialog(
214                    Main.parent,
215                    tr("No changes to upload."),
216                    tr("Warning"),
217                    JOptionPane.INFORMATION_MESSAGE
218            );
219            return;
220        }
221        if (!checkPreUploadConditions(layer, apiData))
222            return;
223
224        final UploadDialog dialog = UploadDialog.getUploadDialog();
225        // If we simply set the changeset comment here, it would be
226        // overridden by subsequent events in EDT that are caused by
227        // dialog creation. The current solution is to queue this operation
228        // after these events.
229        // TODO: find better way to initialize the comment field
230        SwingUtilities.invokeLater(new Runnable() {
231            @Override
232            public void run() {
233                dialog.setDefaultChangesetTags(layer.data.getChangeSetTags());
234            }
235        });
236        dialog.setUploadedPrimitives(apiData);
237        dialog.setVisible(true);
238        if (dialog.isCanceled())
239            return;
240        dialog.rememberUserInput();
241
242        for (UploadHook hook : lateUploadHooks) {
243            if (!hook.checkUpload(apiData))
244                return;
245        }
246
247        Main.worker.execute(
248                new UploadPrimitivesTask(
249                        UploadDialog.getUploadDialog().getUploadStrategySpecification(),
250                        layer,
251                        apiData,
252                        UploadDialog.getUploadDialog().getChangeset()
253                )
254        );
255    }
256
257    @Override
258    public void actionPerformed(ActionEvent e) {
259        if (!isEnabled())
260            return;
261        if (Main.map == null) {
262            JOptionPane.showMessageDialog(
263                    Main.parent,
264                    tr("Nothing to upload. Get some data first."),
265                    tr("Warning"),
266                    JOptionPane.WARNING_MESSAGE
267            );
268            return;
269        }
270        APIDataSet apiData = new APIDataSet(Main.main.getCurrentDataSet());
271        uploadData(Main.main.getEditLayer(), apiData);
272    }
273}