001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.io;
003    
004    import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005    import static org.openstreetmap.josm.tools.CheckParameterUtil.ensureParameterNotNull;
006    import static org.openstreetmap.josm.tools.I18n.tr;
007    import static org.openstreetmap.josm.tools.I18n.trn;
008    
009    import java.io.IOException;
010    import java.lang.reflect.InvocationTargetException;
011    import java.util.HashSet;
012    
013    import javax.swing.JOptionPane;
014    import javax.swing.SwingUtilities;
015    
016    import org.openstreetmap.josm.Main;
017    import org.openstreetmap.josm.data.APIDataSet;
018    import org.openstreetmap.josm.data.osm.Changeset;
019    import org.openstreetmap.josm.data.osm.ChangesetCache;
020    import org.openstreetmap.josm.data.osm.IPrimitive;
021    import org.openstreetmap.josm.data.osm.Node;
022    import org.openstreetmap.josm.data.osm.OsmPrimitive;
023    import org.openstreetmap.josm.data.osm.Relation;
024    import org.openstreetmap.josm.data.osm.Way;
025    import org.openstreetmap.josm.gui.DefaultNameFormatter;
026    import org.openstreetmap.josm.gui.HelpAwareOptionPane;
027    import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
028    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
029    import org.openstreetmap.josm.gui.progress.ProgressMonitor;
030    import org.openstreetmap.josm.gui.util.GuiHelper;
031    import org.openstreetmap.josm.io.ChangesetClosedException;
032    import org.openstreetmap.josm.io.OsmApi;
033    import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
034    import org.openstreetmap.josm.io.OsmServerWriter;
035    import org.openstreetmap.josm.io.OsmTransferCanceledException;
036    import org.openstreetmap.josm.io.OsmTransferException;
037    import org.openstreetmap.josm.tools.ImageProvider;
038    import org.xml.sax.SAXException;
039    
040    /**
041     * The task for uploading a collection of primitives
042     *
043     */
044    public 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 HashSet<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<IPrimitive>();
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                System.out.println(tr("Warning: {0}", 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                public void run() {
224                    layer.cleanupAfterUpload(processedPrimitives);
225                    layer.onPostUploadToServer();
226                    ChangesetCache.getInstance().update(changeset);
227                }
228            };
229    
230            try {
231                SwingUtilities.invokeAndWait(r);
232            } catch(InterruptedException e) {
233                lastException = e;
234            } catch(InvocationTargetException e) {
235                lastException = new OsmTransferException(e.getCause());
236            }
237        }
238    
239        @Override protected void realRun() throws SAXException, IOException {
240            try {
241                uploadloop:while(true) {
242                    try {
243                        getProgressMonitor().subTask(trn("Uploading {0} object...", "Uploading {0} objects...", toUpload.getSize(), toUpload.getSize()));
244                        synchronized(this) {
245                            writer = new OsmServerWriter();
246                        }
247                        writer.uploadOsm(strategy, toUpload.getPrimitives(), changeset, getProgressMonitor().createSubTaskMonitor(1, false));
248    
249                        // if we get here we've successfully uploaded the data. Exit the loop.
250                        //
251                        break;
252                    } catch(OsmTransferCanceledException e) {
253                        e.printStackTrace();
254                        uploadCanceled = true;
255                        break uploadloop;
256                    } catch(OsmApiPrimitiveGoneException e) {
257                        // try to recover from  410 Gone
258                        //
259                        recoverFromGoneOnServer(e, getProgressMonitor());
260                    } catch(ChangesetClosedException e) {
261                        processedPrimitives.addAll(writer.getProcessedPrimitives()); // OsmPrimitive in => OsmPrimitive out
262                        changeset.setOpen(false);
263                        switch(e.getSource()) {
264                        case UNSPECIFIED:
265                            throw e;
266                        case UPDATE_CHANGESET:
267                            // The changeset was closed when we tried to update it. Probably, our
268                            // local list of open changesets got out of sync with the server state.
269                            // The user will have to select another open changeset.
270                            // Rethrow exception - this will be handled later.
271                            //
272                            throw e;
273                        case UPLOAD_DATA:
274                            // Most likely the changeset is full. Try to recover and continue
275                            // with a new changeset, but let the user decide first (see
276                            // recoverFromChangesetFullException)
277                            //
278                            if (recoverFromChangesetFullException()) {
279                                continue;
280                            }
281                            lastException = e;
282                            break uploadloop;
283                        }
284                    } finally {
285                        if (writer != null) {
286                            processedPrimitives.addAll(writer.getProcessedPrimitives());
287                        }
288                        synchronized(this) {
289                            writer = null;
290                        }
291                    }
292                }
293            // if required close the changeset
294            //
295            if (strategy.isCloseChangesetAfterUpload() && changeset != null && !changeset.isNew() && changeset.isOpen()) {
296                OsmApi.getOsmApi().closeChangeset(changeset, progressMonitor.createSubTaskMonitor(0, false));
297            }
298            } catch (Exception e) {
299                if (uploadCanceled) {
300                    System.out.println(tr("Ignoring caught exception because upload is canceled. Exception is: {0}", e.toString()));
301                } else {
302                    lastException = e;
303                }
304            }
305            if (uploadCanceled && processedPrimitives.isEmpty()) return;
306            cleanupAfterUpload();
307        }
308    
309        @Override protected void finish() {
310            if (uploadCanceled)
311                return;
312    
313            // depending on the success of the upload operation and on the policy for
314            // multi changeset uploads this will sent the user back to the appropriate
315            // place in JOSM, either
316            // - to an error dialog
317            // - to the Upload Dialog
318            // - to map editing
319            GuiHelper.runInEDT(new Runnable() {
320                public void run() {
321                    // if the changeset is still open after this upload we want it to
322                    // be selected on the next upload
323                    //
324                    ChangesetCache.getInstance().update(changeset);
325                    if (changeset != null && changeset.isOpen()) {
326                        UploadDialog.getUploadDialog().setSelectedChangesetForNextUpload(changeset);
327                    }
328                    if (lastException == null)
329                        return;
330                    if (lastException instanceof ChangesetClosedException) {
331                        ChangesetClosedException e = (ChangesetClosedException)lastException;
332                        if (e.getSource().equals(ChangesetClosedException.Source.UPDATE_CHANGESET)) {
333                            handleFailedUpload(lastException);
334                            return;
335                        }
336                        if (strategy.getPolicy() == null)
337                            /* do nothing if unknown policy */
338                            return;
339                        if (e.getSource().equals(ChangesetClosedException.Source.UPLOAD_DATA)) {
340                            switch(strategy.getPolicy()) {
341                            case ABORT:
342                                break; /* do nothing - we return to map editing */
343                            case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
344                                break; /* do nothing - we return to map editing */
345                            case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
346                                // return to the upload dialog
347                                //
348                                toUpload.removeProcessed(processedPrimitives);
349                                UploadDialog.getUploadDialog().setUploadedPrimitives(toUpload);
350                                UploadDialog.getUploadDialog().setVisible(true);
351                                break;
352                            }
353                        } else {
354                            handleFailedUpload(lastException);
355                        }
356                    } else {
357                        handleFailedUpload(lastException);
358                    }
359                }
360            });
361        }
362    
363        @Override protected void cancel() {
364            uploadCanceled = true;
365            synchronized(this) {
366                if (writer != null) {
367                    writer.cancel();
368                }
369            }
370        }
371    }