001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.CheckParameterUtil.ensureParameterNotNull;
006import static org.openstreetmap.josm.tools.I18n.tr;
007import static org.openstreetmap.josm.tools.I18n.trn;
008
009import java.lang.reflect.InvocationTargetException;
010import java.util.HashSet;
011import java.util.Set;
012
013import javax.swing.JOptionPane;
014import javax.swing.SwingUtilities;
015
016import org.openstreetmap.josm.Main;
017import org.openstreetmap.josm.data.APIDataSet;
018import org.openstreetmap.josm.data.osm.Changeset;
019import org.openstreetmap.josm.data.osm.ChangesetCache;
020import org.openstreetmap.josm.data.osm.IPrimitive;
021import org.openstreetmap.josm.data.osm.Node;
022import org.openstreetmap.josm.data.osm.OsmPrimitive;
023import org.openstreetmap.josm.data.osm.Relation;
024import org.openstreetmap.josm.data.osm.Way;
025import org.openstreetmap.josm.gui.DefaultNameFormatter;
026import org.openstreetmap.josm.gui.HelpAwareOptionPane;
027import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
028import org.openstreetmap.josm.gui.Notification;
029import org.openstreetmap.josm.gui.layer.OsmDataLayer;
030import org.openstreetmap.josm.gui.progress.ProgressMonitor;
031import org.openstreetmap.josm.gui.util.GuiHelper;
032import org.openstreetmap.josm.io.ChangesetClosedException;
033import org.openstreetmap.josm.io.OsmApi;
034import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
035import org.openstreetmap.josm.io.OsmServerWriter;
036import org.openstreetmap.josm.io.OsmTransferCanceledException;
037import org.openstreetmap.josm.io.OsmTransferException;
038import org.openstreetmap.josm.tools.ImageProvider;
039
040/**
041 * The task for uploading a collection of primitives.
042 *
043 */
044public class UploadPrimitivesTask extends AbstractUploadTask {
045    private boolean uploadCanceled = false;
046    private Exception lastException = null;
047    private APIDataSet toUpload;
048    private OsmServerWriter writer;
049    private OsmDataLayer layer;
050    private Changeset changeset;
051    private Set<IPrimitive> processedPrimitives;
052    private UploadStrategySpecification strategy;
053
054    /**
055     * Creates the task
056     *
057     * @param strategy the upload strategy. Must not be null.
058     * @param layer  the OSM data layer for which data is uploaded. Must not be null.
059     * @param toUpload the collection of primitives to upload. Set to the empty collection if null.
060     * @param changeset the changeset to use for uploading. Must not be null. changeset.getId()
061     * can be 0 in which case the upload task creates a new changeset
062     * @throws IllegalArgumentException thrown if layer is null
063     * @throws IllegalArgumentException thrown if toUpload is null
064     * @throws IllegalArgumentException thrown if strategy is null
065     * @throws IllegalArgumentException thrown if changeset is null
066     */
067    public UploadPrimitivesTask(UploadStrategySpecification strategy, OsmDataLayer layer, APIDataSet toUpload, Changeset changeset) {
068        super(tr("Uploading data for layer ''{0}''", layer.getName()),false /* don't ignore exceptions */);
069        ensureParameterNotNull(layer,"layer");
070        ensureParameterNotNull(strategy, "strategy");
071        ensureParameterNotNull(changeset, "changeset");
072        this.toUpload = toUpload;
073        this.layer = layer;
074        this.changeset = changeset;
075        this.strategy = strategy;
076        this.processedPrimitives = new HashSet<>();
077    }
078
079    protected MaxChangesetSizeExceededPolicy askMaxChangesetSizeExceedsPolicy() {
080        ButtonSpec[] specs = new ButtonSpec[] {
081                new ButtonSpec(
082                        tr("Continue uploading"),
083                        ImageProvider.get("upload"),
084                        tr("Click to continue uploading to additional new changesets"),
085                        null /* no specific help text */
086                ),
087                new ButtonSpec(
088                        tr("Go back to Upload Dialog"),
089                        ImageProvider.get("dialogs", "uploadproperties"),
090                        tr("Click to return to the Upload Dialog"),
091                        null /* no specific help text */
092                ),
093                new ButtonSpec(
094                        tr("Abort"),
095                        ImageProvider.get("cancel"),
096                        tr("Click to abort uploading"),
097                        null /* no specific help text */
098                )
099        };
100        int numObjectsToUploadLeft = toUpload.getSize() - processedPrimitives.size();
101        String msg1 = tr("The server reported that the current changeset was closed.<br>"
102                + "This is most likely because the changesets size exceeded the max. size<br>"
103                + "of {0} objects on the server ''{1}''.",
104                OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(),
105                OsmApi.getOsmApi().getBaseUrl()
106        );
107        String msg2 = trn(
108                "There is {0} object left to upload.",
109                "There are {0} objects left to upload.",
110                numObjectsToUploadLeft,
111                numObjectsToUploadLeft
112        );
113        String msg3 = tr(
114                "Click ''<strong>{0}</strong>'' to continue uploading to additional new changesets.<br>"
115                + "Click ''<strong>{1}</strong>'' to return to the upload dialog.<br>"
116                + "Click ''<strong>{2}</strong>'' to abort uploading and return to map editing.<br>",
117                specs[0].text,
118                specs[1].text,
119                specs[2].text
120        );
121        String msg = "<html>" + msg1 + "<br><br>" + msg2 +"<br><br>" + msg3 + "</html>";
122        int ret = HelpAwareOptionPane.showOptionDialog(
123                Main.parent,
124                msg,
125                tr("Changeset is full"),
126                JOptionPane.WARNING_MESSAGE,
127                null, /* no special icon */
128                specs,
129                specs[0],
130                ht("/Action/Upload#ChangesetFull")
131        );
132        switch(ret) {
133        case 0: return MaxChangesetSizeExceededPolicy.AUTOMATICALLY_OPEN_NEW_CHANGESETS;
134        case 1: return MaxChangesetSizeExceededPolicy.FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG;
135        case 2: return MaxChangesetSizeExceededPolicy.ABORT;
136        case JOptionPane.CLOSED_OPTION: return MaxChangesetSizeExceededPolicy.ABORT;
137        }
138        // should not happen
139        return null;
140    }
141
142    protected void openNewChangeset() {
143        // make sure the current changeset is removed from the upload dialog.
144        //
145        ChangesetCache.getInstance().update(changeset);
146        Changeset newChangeSet = new Changeset();
147        newChangeSet.setKeys(this.changeset.getKeys());
148        this.changeset = newChangeSet;
149    }
150
151    protected boolean recoverFromChangesetFullException() {
152        if (toUpload.getSize() - processedPrimitives.size() == 0) {
153            strategy.setPolicy(MaxChangesetSizeExceededPolicy.ABORT);
154            return false;
155        }
156        if (strategy.getPolicy() == null || strategy.getPolicy().equals(MaxChangesetSizeExceededPolicy.ABORT)) {
157            MaxChangesetSizeExceededPolicy policy = askMaxChangesetSizeExceedsPolicy();
158            strategy.setPolicy(policy);
159        }
160        switch(strategy.getPolicy()) {
161        case ABORT:
162            // don't continue - finish() will send the user back to map editing
163            //
164            return false;
165        case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
166            // don't continue - finish() will send the user back to the upload dialog
167            //
168            return false;
169        case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
170            // prepare the state of the task for a next iteration in uploading.
171            //
172            openNewChangeset();
173            toUpload.removeProcessed(processedPrimitives);
174            return true;
175        }
176        // should not happen
177        return false;
178    }
179
180    /**
181     * Retries to recover the upload operation from an exception which was thrown because
182     * an uploaded primitive was already deleted on the server.
183     *
184     * @param e the exception throw by the API
185     * @param monitor a progress monitor
186     * @throws OsmTransferException  thrown if we can't recover from the exception
187     */
188    protected void recoverFromGoneOnServer(OsmApiPrimitiveGoneException e, ProgressMonitor monitor) throws OsmTransferException{
189        if (!e.isKnownPrimitive()) throw e;
190        OsmPrimitive p = layer.data.getPrimitiveById(e.getPrimitiveId(), e.getPrimitiveType());
191        if (p == null) throw e;
192        if (p.isDeleted()) {
193            // we tried to delete an already deleted primitive.
194            final String msg;
195            final String displayName = p.getDisplayName(DefaultNameFormatter.getInstance());
196            if (p instanceof Node) {
197                msg = tr("Node ''{0}'' is already deleted. Skipping object in upload.", displayName);
198            } else if (p instanceof Way) {
199                msg = tr("Way ''{0}'' is already deleted. Skipping object in upload.", displayName);
200            } else if (p instanceof Relation) {
201                msg = tr("Relation ''{0}'' is already deleted. Skipping object in upload.", displayName);
202            } else {
203                msg = tr("Object ''{0}'' is already deleted. Skipping object in upload.", displayName);
204            }
205            monitor.appendLogMessage(msg);
206            Main.warn(msg);
207            processedPrimitives.addAll(writer.getProcessedPrimitives());
208            processedPrimitives.add(p);
209            toUpload.removeProcessed(processedPrimitives);
210            return;
211        }
212        // exception was thrown because we tried to *update* an already deleted
213        // primitive. We can't resolve this automatically. Re-throw exception,
214        // a conflict is going to be created later.
215        throw e;
216    }
217
218    protected void cleanupAfterUpload() {
219        // we always clean up the data, even in case of errors. It's possible the data was
220        // partially uploaded. Better run on EDT.
221        //
222        Runnable r  = new Runnable() {
223            @Override
224            public void run() {
225                layer.cleanupAfterUpload(processedPrimitives);
226                layer.onPostUploadToServer();
227                ChangesetCache.getInstance().update(changeset);
228            }
229        };
230
231        try {
232            SwingUtilities.invokeAndWait(r);
233        } catch(InterruptedException e) {
234            lastException = e;
235        } catch(InvocationTargetException e) {
236            lastException = new OsmTransferException(e.getCause());
237        }
238    }
239
240    @Override protected void realRun() {
241        try {
242            uploadloop:while(true) {
243                try {
244                    getProgressMonitor().subTask(trn("Uploading {0} object...", "Uploading {0} objects...", toUpload.getSize(), toUpload.getSize()));
245                    synchronized(this) {
246                        writer = new OsmServerWriter();
247                    }
248                    writer.uploadOsm(strategy, toUpload.getPrimitives(), changeset, getProgressMonitor().createSubTaskMonitor(1, false));
249
250                    // if we get here we've successfully uploaded the data. Exit the loop.
251                    //
252                    break;
253                } catch(OsmTransferCanceledException e) {
254                    Main.error(e);
255                    uploadCanceled = true;
256                    break uploadloop;
257                } catch(OsmApiPrimitiveGoneException e) {
258                    // try to recover from  410 Gone
259                    //
260                    recoverFromGoneOnServer(e, getProgressMonitor());
261                } catch(ChangesetClosedException e) {
262                    processedPrimitives.addAll(writer.getProcessedPrimitives()); // OsmPrimitive in => OsmPrimitive out
263                    changeset.setOpen(false);
264                    switch(e.getSource()) {
265                    case UNSPECIFIED:
266                        throw e;
267                    case UPDATE_CHANGESET:
268                        // The changeset was closed when we tried to update it. Probably, our
269                        // local list of open changesets got out of sync with the server state.
270                        // The user will have to select another open changeset.
271                        // Rethrow exception - this will be handled later.
272                        //
273                        throw e;
274                    case UPLOAD_DATA:
275                        // Most likely the changeset is full. Try to recover and continue
276                        // with a new changeset, but let the user decide first (see
277                        // recoverFromChangesetFullException)
278                        //
279                        if (recoverFromChangesetFullException()) {
280                            continue;
281                        }
282                        lastException = e;
283                        break uploadloop;
284                    }
285                } finally {
286                    if (writer != null) {
287                        processedPrimitives.addAll(writer.getProcessedPrimitives());
288                    }
289                    synchronized(this) {
290                        writer = null;
291                    }
292                }
293            }
294            // if required close the changeset
295            //
296            if (strategy.isCloseChangesetAfterUpload() && changeset != null && !changeset.isNew() && changeset.isOpen()) {
297                OsmApi.getOsmApi().closeChangeset(changeset, progressMonitor.createSubTaskMonitor(0, false));
298            }
299        } catch (OsmTransferException e) {
300            if (uploadCanceled) {
301                Main.info(tr("Ignoring caught exception because upload is canceled. Exception is: {0}", e.toString()));
302            } else {
303                lastException = e;
304            }
305        }
306        if (uploadCanceled && processedPrimitives.isEmpty()) return;
307        cleanupAfterUpload();
308    }
309
310    @Override protected void finish() {
311        if (uploadCanceled)
312            return;
313
314        // depending on the success of the upload operation and on the policy for
315        // multi changeset uploads this will sent the user back to the appropriate
316        // place in JOSM, either
317        // - to an error dialog
318        // - to the Upload Dialog
319        // - to map editing
320        GuiHelper.runInEDT(new Runnable() {
321            @Override
322            public void run() {
323                // if the changeset is still open after this upload we want it to
324                // be selected on the next upload
325                //
326                ChangesetCache.getInstance().update(changeset);
327                if (changeset != null && changeset.isOpen()) {
328                    UploadDialog.getUploadDialog().setSelectedChangesetForNextUpload(changeset);
329                }
330                if (lastException == null) {
331                    new Notification(
332                            "<h3>" + tr("Upload successful!") + "</h3>")
333                            .setIcon(ImageProvider.get("misc", "check_large"))
334                            .show();
335                    return;
336                }
337                if (lastException instanceof ChangesetClosedException) {
338                    ChangesetClosedException e = (ChangesetClosedException)lastException;
339                    if (e.getSource().equals(ChangesetClosedException.Source.UPDATE_CHANGESET)) {
340                        handleFailedUpload(lastException);
341                        return;
342                    }
343                    if (strategy.getPolicy() == null)
344                        /* do nothing if unknown policy */
345                        return;
346                    if (e.getSource().equals(ChangesetClosedException.Source.UPLOAD_DATA)) {
347                        switch(strategy.getPolicy()) {
348                        case ABORT:
349                            break; /* do nothing - we return to map editing */
350                        case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
351                            break; /* do nothing - we return to map editing */
352                        case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
353                            // return to the upload dialog
354                            //
355                            toUpload.removeProcessed(processedPrimitives);
356                            UploadDialog.getUploadDialog().setUploadedPrimitives(toUpload);
357                            UploadDialog.getUploadDialog().setVisible(true);
358                            break;
359                        }
360                    } else {
361                        handleFailedUpload(lastException);
362                    }
363                } else {
364                    handleFailedUpload(lastException);
365                }
366            }
367        });
368    }
369
370    @Override protected void cancel() {
371        uploadCanceled = true;
372        synchronized(this) {
373            if (writer != null) {
374                writer.cancel();
375            }
376        }
377    }
378}