source: josm/trunk/src/org/openstreetmap/josm/gui/io/UploadPrimitivesTask.java @ 13453

Last change on this file since 13453 was 13453, checked in by Don-vip, 5 months ago

fix #8039, fix #10456: final fixes for the read-only/locked layers:

  • rename "read-only" to "locked" (in XML and Java classes/interfaces)
  • add a new download policy (true/never) to allow private layers forbidding only to download data, but allowing everything else

This leads to:

  • normal layers: download allowed, modifications allowed, upload allowed
  • private layers: download allowed or not (download=true/never), modifications allowed, upload allowed or not (upload=true/discouraged/never)
  • locked layers: nothing allowed, the data cannot be modified in any way
  • Property svn:eol-style set to native
File size: 17.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.io;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.CheckParameterUtil.ensureParameterNotNull;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.lang.reflect.InvocationTargetException;
10import java.util.HashSet;
11import java.util.Set;
12
13import javax.swing.JOptionPane;
14import javax.swing.SwingUtilities;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.data.APIDataSet;
18import org.openstreetmap.josm.data.osm.Changeset;
19import org.openstreetmap.josm.data.osm.ChangesetCache;
20import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
21import org.openstreetmap.josm.data.osm.IPrimitive;
22import org.openstreetmap.josm.data.osm.Node;
23import org.openstreetmap.josm.data.osm.OsmPrimitive;
24import org.openstreetmap.josm.data.osm.Relation;
25import org.openstreetmap.josm.data.osm.Way;
26import org.openstreetmap.josm.gui.HelpAwareOptionPane;
27import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
28import org.openstreetmap.josm.gui.Notification;
29import org.openstreetmap.josm.gui.layer.OsmDataLayer;
30import org.openstreetmap.josm.gui.progress.ProgressMonitor;
31import org.openstreetmap.josm.gui.util.GuiHelper;
32import org.openstreetmap.josm.gui.widgets.HtmlPanel;
33import org.openstreetmap.josm.io.ChangesetClosedException;
34import org.openstreetmap.josm.io.MaxChangesetSizeExceededPolicy;
35import org.openstreetmap.josm.io.MessageNotifier;
36import org.openstreetmap.josm.io.OsmApi;
37import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
38import org.openstreetmap.josm.io.OsmServerWriter;
39import org.openstreetmap.josm.io.OsmTransferCanceledException;
40import org.openstreetmap.josm.io.OsmTransferException;
41import org.openstreetmap.josm.io.UploadStrategySpecification;
42import org.openstreetmap.josm.tools.ImageProvider;
43import org.openstreetmap.josm.tools.Logging;
44
45/**
46 * The task for uploading a collection of primitives.
47 * @since 2599
48 */
49public class UploadPrimitivesTask extends AbstractUploadTask {
50    private boolean uploadCanceled;
51    private Exception lastException;
52    private final APIDataSet toUpload;
53    private OsmServerWriter writer;
54    private final OsmDataLayer layer;
55    private Changeset changeset;
56    private final Set<IPrimitive> processedPrimitives;
57    private final UploadStrategySpecification strategy;
58
59    /**
60     * Creates the task
61     *
62     * @param strategy the upload strategy. Must not be null.
63     * @param layer  the OSM data layer for which data is uploaded. Must not be null.
64     * @param toUpload the collection of primitives to upload. Set to the empty collection if null.
65     * @param changeset the changeset to use for uploading. Must not be null. changeset.getId()
66     * can be 0 in which case the upload task creates a new changeset
67     * @throws IllegalArgumentException if layer is null
68     * @throws IllegalArgumentException if toUpload is null
69     * @throws IllegalArgumentException if strategy is null
70     * @throws IllegalArgumentException if changeset is null
71     */
72    public UploadPrimitivesTask(UploadStrategySpecification strategy, OsmDataLayer layer, APIDataSet toUpload, Changeset changeset) {
73        super(tr("Uploading data for layer ''{0}''", layer.getName()), false /* don't ignore exceptions */);
74        ensureParameterNotNull(layer, "layer");
75        ensureParameterNotNull(strategy, "strategy");
76        ensureParameterNotNull(changeset, "changeset");
77        this.toUpload = toUpload;
78        this.layer = layer;
79        this.changeset = changeset;
80        this.strategy = strategy;
81        this.processedPrimitives = new HashSet<>();
82    }
83
84    protected MaxChangesetSizeExceededPolicy askMaxChangesetSizeExceedsPolicy() {
85        ButtonSpec[] specs = new ButtonSpec[] {
86                new ButtonSpec(
87                        tr("Continue uploading"),
88                        ImageProvider.get("upload"),
89                        tr("Click to continue uploading to additional new changesets"),
90                        null /* no specific help text */
91                ),
92                new ButtonSpec(
93                        tr("Go back to Upload Dialog"),
94                        ImageProvider.get("dialogs", "uploadproperties"),
95                        tr("Click to return to the Upload Dialog"),
96                        null /* no specific help text */
97                ),
98                new ButtonSpec(
99                        tr("Abort"),
100                        ImageProvider.get("cancel"),
101                        tr("Click to abort uploading"),
102                        null /* no specific help text */
103                )
104        };
105        int numObjectsToUploadLeft = toUpload.getSize() - processedPrimitives.size();
106        String msg1 = tr("The server reported that the current changeset was closed.<br>"
107                + "This is most likely because the changesets size exceeded the max. size<br>"
108                + "of {0} objects on the server ''{1}''.",
109                OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(),
110                OsmApi.getOsmApi().getBaseUrl()
111        );
112        String msg2 = trn(
113                "There is {0} object left to upload.",
114                "There are {0} objects left to upload.",
115                numObjectsToUploadLeft,
116                numObjectsToUploadLeft
117        );
118        String msg3 = tr(
119                "Click ''<strong>{0}</strong>'' to continue uploading to additional new changesets.<br>"
120                + "Click ''<strong>{1}</strong>'' to return to the upload dialog.<br>"
121                + "Click ''<strong>{2}</strong>'' to abort uploading and return to map editing.<br>",
122                specs[0].text,
123                specs[1].text,
124                specs[2].text
125        );
126        String msg = "<html>" + msg1 + "<br><br>" + msg2 +"<br><br>" + msg3 + "</html>";
127        int ret = HelpAwareOptionPane.showOptionDialog(
128                Main.parent,
129                msg,
130                tr("Changeset is full"),
131                JOptionPane.WARNING_MESSAGE,
132                null, /* no special icon */
133                specs,
134                specs[0],
135                ht("/Action/Upload#ChangesetFull")
136        );
137        switch(ret) {
138        case 0: return MaxChangesetSizeExceededPolicy.AUTOMATICALLY_OPEN_NEW_CHANGESETS;
139        case 1: return MaxChangesetSizeExceededPolicy.FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG;
140        case 2:
141        case JOptionPane.CLOSED_OPTION:
142        default: return MaxChangesetSizeExceededPolicy.ABORT;
143        }
144    }
145
146    /**
147     * Opens a new changeset.
148     */
149    protected void openNewChangeset() {
150        // make sure the current changeset is removed from the upload dialog.
151        ChangesetCache.getInstance().update(changeset);
152        Changeset newChangeSet = new Changeset();
153        newChangeSet.setKeys(this.changeset.getKeys());
154        this.changeset = newChangeSet;
155    }
156
157    protected boolean recoverFromChangesetFullException() throws OsmTransferException {
158        if (toUpload.getSize() - processedPrimitives.size() == 0) {
159            strategy.setPolicy(MaxChangesetSizeExceededPolicy.ABORT);
160            return false;
161        }
162        if (strategy.getPolicy() == null || strategy.getPolicy().equals(MaxChangesetSizeExceededPolicy.ABORT)) {
163            strategy.setPolicy(askMaxChangesetSizeExceedsPolicy());
164        }
165        switch(strategy.getPolicy()) {
166        case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
167            // prepare the state of the task for a next iteration in uploading.
168            closeChangesetIfRequired();
169            openNewChangeset();
170            toUpload.removeProcessed(processedPrimitives);
171            return true;
172        case ABORT:
173        case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
174        default:
175            // don't continue - finish() will send the user back to map editing or upload dialog
176            return false;
177        }
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 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            Logging.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        Runnable r = () -> {
222            boolean readOnly = layer.isLocked();
223            if (readOnly) {
224                layer.unlock();
225            }
226            try {
227                layer.cleanupAfterUpload(processedPrimitives);
228                layer.onPostUploadToServer();
229                ChangesetCache.getInstance().update(changeset);
230            } finally {
231                if (readOnly) {
232                    layer.lock();
233                }
234            }
235        };
236
237        try {
238            SwingUtilities.invokeAndWait(r);
239        } catch (InterruptedException e) {
240            Logging.trace(e);
241            lastException = e;
242            Thread.currentThread().interrupt();
243        } catch (InvocationTargetException e) {
244            Logging.trace(e);
245            lastException = new OsmTransferException(e.getCause());
246        }
247    }
248
249    @Override
250    protected void realRun() {
251        try {
252            MessageNotifier.stop();
253            uploadloop: while (true) {
254                try {
255                    getProgressMonitor().subTask(
256                            trn("Uploading {0} object...", "Uploading {0} objects...", toUpload.getSize(), toUpload.getSize()));
257                    synchronized (this) {
258                        writer = new OsmServerWriter();
259                    }
260                    writer.uploadOsm(strategy, toUpload.getPrimitives(), changeset, getProgressMonitor().createSubTaskMonitor(1, false));
261
262                    // if we get here we've successfully uploaded the data. Exit the loop.
263                    break;
264                } catch (OsmTransferCanceledException e) {
265                    Logging.error(e);
266                    uploadCanceled = true;
267                    break uploadloop;
268                } catch (OsmApiPrimitiveGoneException e) {
269                    // try to recover from  410 Gone
270                    recoverFromGoneOnServer(e, getProgressMonitor());
271                } catch (ChangesetClosedException e) {
272                    if (writer != null) {
273                        processedPrimitives.addAll(writer.getProcessedPrimitives()); // OsmPrimitive in => OsmPrimitive out
274                    }
275                    switch(e.getSource()) {
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                        if (recoverFromChangesetFullException()) {
281                            continue;
282                        }
283                        lastException = e;
284                        break uploadloop;
285                    case UNSPECIFIED:
286                    case UPDATE_CHANGESET:
287                    default:
288                        // The changeset was closed when we tried to update it. Probably, our
289                        // local list of open changesets got out of sync with the server state.
290                        // The user will have to select another open changeset.
291                        // Rethrow exception - this will be handled later.
292                        changeset.setOpen(false);
293                        throw e;
294                    }
295                } finally {
296                    if (writer != null) {
297                        processedPrimitives.addAll(writer.getProcessedPrimitives());
298                    }
299                    synchronized (this) {
300                        writer = null;
301                    }
302                }
303            }
304            // if required close the changeset
305            closeChangesetIfRequired();
306        } catch (OsmTransferException e) {
307            if (uploadCanceled) {
308                Logging.info(tr("Ignoring caught exception because upload is canceled. Exception is: {0}", e.toString()));
309            } else {
310                lastException = e;
311            }
312        } finally {
313            if (MessageNotifier.PROP_NOTIFIER_ENABLED.get()) {
314                MessageNotifier.start();
315            }
316        }
317        if (uploadCanceled && processedPrimitives.isEmpty()) return;
318        cleanupAfterUpload();
319    }
320
321    private void closeChangesetIfRequired() throws OsmTransferException {
322        if (strategy.isCloseChangesetAfterUpload() && changeset != null && !changeset.isNew() && changeset.isOpen()) {
323            OsmApi.getOsmApi().closeChangeset(changeset, progressMonitor.createSubTaskMonitor(0, false));
324        }
325    }
326
327    @Override protected void finish() {
328
329        // depending on the success of the upload operation and on the policy for
330        // multi changeset uploads this will sent the user back to the appropriate
331        // place in JOSM, either
332        // - to an error dialog
333        // - to the Upload Dialog
334        // - to map editing
335        GuiHelper.runInEDT(() -> {
336            // if the changeset is still open after this upload we want it to be selected on the next upload
337            ChangesetCache.getInstance().update(changeset);
338            if (changeset != null && changeset.isOpen()) {
339                UploadDialog.getUploadDialog().setSelectedChangesetForNextUpload(changeset);
340            }
341            if (uploadCanceled) return;
342            if (lastException == null) {
343                HtmlPanel panel = new HtmlPanel(
344                        "<h3><a href=\"" + Main.getBaseBrowseUrl() + "/changeset/" + changeset.getId() + "\">"
345                                + tr("Upload successful!") + "</a></h3>");
346                panel.enableClickableHyperlinks();
347                panel.setOpaque(false);
348                new Notification()
349                        .setContent(panel)
350                        .setIcon(ImageProvider.get("misc", "check_large"))
351                        .show();
352                return;
353            }
354            if (lastException instanceof ChangesetClosedException) {
355                ChangesetClosedException e = (ChangesetClosedException) lastException;
356                if (e.getSource().equals(ChangesetClosedException.Source.UPDATE_CHANGESET)) {
357                    handleFailedUpload(lastException);
358                    return;
359                }
360                if (strategy.getPolicy() == null)
361                    /* do nothing if unknown policy */
362                    return;
363                if (e.getSource().equals(ChangesetClosedException.Source.UPLOAD_DATA)) {
364                    switch(strategy.getPolicy()) {
365                    case ABORT:
366                        break; /* do nothing - we return to map editing */
367                    case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
368                        break; /* do nothing - we return to map editing */
369                    case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
370                        // return to the upload dialog
371                        //
372                        toUpload.removeProcessed(processedPrimitives);
373                        UploadDialog.getUploadDialog().setUploadedPrimitives(toUpload);
374                        UploadDialog.getUploadDialog().setVisible(true);
375                        break;
376                    }
377                } else {
378                    handleFailedUpload(lastException);
379                }
380            } else {
381                handleFailedUpload(lastException);
382            }
383        });
384    }
385
386    @Override protected void cancel() {
387        uploadCanceled = true;
388        synchronized (this) {
389            if (writer != null) {
390                writer.cancel();
391            }
392        }
393    }
394}
Note: See TracBrowser for help on using the repository browser.