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