001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.Collection;
007import java.util.HashSet;
008import java.util.Optional;
009import java.util.Set;
010
011import org.openstreetmap.josm.data.APIDataSet;
012import org.openstreetmap.josm.data.osm.Changeset;
013import org.openstreetmap.josm.data.osm.CyclicUploadDependencyException;
014import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
015import org.openstreetmap.josm.data.osm.IPrimitive;
016import org.openstreetmap.josm.data.osm.OsmPrimitive;
017import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
018import org.openstreetmap.josm.gui.layer.OsmDataLayer;
019import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
020import org.openstreetmap.josm.gui.progress.ProgressMonitor;
021import org.openstreetmap.josm.io.OsmApi;
022import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
023import org.openstreetmap.josm.io.OsmServerWriter;
024import org.openstreetmap.josm.io.OsmTransferException;
025import org.openstreetmap.josm.io.UploadStrategySpecification;
026import org.openstreetmap.josm.tools.CheckParameterUtil;
027import org.openstreetmap.josm.tools.Logging;
028
029/**
030 * UploadLayerTask uploads the data managed by an {@link OsmDataLayer} asynchronously.
031 *
032 * <pre>
033 *     ExecutorService executorService = ...
034 *     UploadLayerTask task = new UploadLayerTask(layer, monitor);
035 *     Future&lt;?&gt; taskFuture = executorService.submit(task)
036 *     try {
037 *        // wait for the task to complete
038 *        taskFuture.get();
039 *     } catch (Exception e) {
040 *        e.printStackTrace();
041 *     }
042 * </pre>
043 */
044public class UploadLayerTask extends AbstractIOTask {
045    private OsmServerWriter writer;
046    private final OsmDataLayer layer;
047    private final ProgressMonitor monitor;
048    private final Changeset changeset;
049    private Collection<OsmPrimitive> toUpload;
050    private final Set<IPrimitive> processedPrimitives;
051    private final UploadStrategySpecification strategy;
052
053    /**
054     * Creates the upload task
055     *
056     * @param strategy the upload strategy specification
057     * @param layer the layer. Must not be null.
058     * @param monitor  a progress monitor. If monitor is null, uses {@link NullProgressMonitor#INSTANCE}
059     * @param changeset the changeset to be used
060     * @throws IllegalArgumentException if layer is null
061     * @throws IllegalArgumentException if strategy is null
062     */
063    public UploadLayerTask(UploadStrategySpecification strategy, OsmDataLayer layer, ProgressMonitor monitor, Changeset changeset) {
064        CheckParameterUtil.ensureParameterNotNull(layer, "layer");
065        CheckParameterUtil.ensureParameterNotNull(strategy, "strategy");
066        this.layer = layer;
067        this.monitor = Optional.ofNullable(monitor).orElse(NullProgressMonitor.INSTANCE);
068        this.changeset = changeset;
069        this.strategy = strategy;
070        processedPrimitives = new HashSet<>();
071    }
072
073    protected OsmPrimitive getPrimitive(OsmPrimitiveType type, long id) {
074        for (OsmPrimitive p: toUpload) {
075            if (OsmPrimitiveType.from(p).equals(type) && p.getId() == id)
076                return p;
077        }
078        return null;
079    }
080
081    /**
082     * Retries to recover the upload operation from an exception which was thrown because
083     * an uploaded primitive was already deleted on the server.
084     *
085     * @param e the exception throw by the API
086     * @throws OsmTransferException if we can't recover from the exception
087     */
088    protected void recoverFromGoneOnServer(OsmApiPrimitiveGoneException e) throws OsmTransferException {
089        if (!e.isKnownPrimitive()) throw e;
090        OsmPrimitive p = getPrimitive(e.getPrimitiveType(), e.getPrimitiveId());
091        if (p == null) throw e;
092        if (p.isDeleted()) {
093            // we tried to delete an already deleted primitive.
094            Logging.warn(tr("Object ''{0}'' is already deleted on the server. Skipping this object and retrying to upload.",
095                    p.getDisplayName(DefaultNameFormatter.getInstance())));
096            processedPrimitives.addAll(writer.getProcessedPrimitives());
097            processedPrimitives.add(p);
098            toUpload.removeAll(processedPrimitives);
099            return;
100        }
101        // exception was thrown because we tried to *update* an already deleted primitive. We can't resolve this automatically.
102        // Re-throw exception, a conflict is going to be created later.
103        throw e;
104    }
105
106    @Override
107    public void run() {
108        monitor.indeterminateSubTask(tr("Preparing objects to upload ..."));
109        APIDataSet ds = new APIDataSet(layer.data);
110        try {
111            ds.adjustRelationUploadOrder();
112        } catch (CyclicUploadDependencyException e) {
113            setLastException(e);
114            return;
115        }
116        toUpload = ds.getPrimitives();
117        if (toUpload.isEmpty())
118            return;
119        writer = new OsmServerWriter();
120        try {
121            while (true) {
122                try {
123                    ProgressMonitor m = monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false);
124                    if (isCanceled()) return;
125                    writer.uploadOsm(strategy, toUpload, changeset, m);
126                    processedPrimitives.addAll(writer.getProcessedPrimitives()); // OsmPrimitive in => OsmPrimitive out
127                    break;
128                } catch (OsmApiPrimitiveGoneException e) {
129                    recoverFromGoneOnServer(e);
130                }
131            }
132            if (strategy.isCloseChangesetAfterUpload() && changeset != null && changeset.getId() > 0) {
133                OsmApi.getOsmApi().closeChangeset(changeset, monitor.createSubTaskMonitor(0, false));
134            }
135        } catch (OsmTransferException sxe) {
136            if (isCanceled()) {
137                Logging.info("Ignoring exception caught because upload is canceled. Exception is: " + sxe);
138                return;
139            }
140            setLastException(sxe);
141        }
142
143        if (isCanceled())
144            return;
145        layer.cleanupAfterUpload(processedPrimitives);
146        layer.onPostUploadToServer();
147
148        // don't process exceptions remembered with setLastException().
149        // Caller is supposed to deal with them.
150    }
151
152    @Override
153    public void cancel() {
154        setCanceled(true);
155        if (writer != null) {
156            writer.cancel();
157        }
158    }
159}