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        return toUpload.stream()
075                .filter(p -> OsmPrimitiveType.from(p) == type && p.getId() == id)
076                .findFirst().orElse(null);
077    }
078
079    /**
080     * Retries to recover the upload operation from an exception which was thrown because
081     * an uploaded primitive was already deleted on the server.
082     *
083     * @param e the exception throw by the API
084     * @throws OsmTransferException if we can't recover from the exception
085     */
086    protected void recoverFromGoneOnServer(OsmApiPrimitiveGoneException e) throws OsmTransferException {
087        if (!e.isKnownPrimitive()) throw e;
088        OsmPrimitive p = getPrimitive(e.getPrimitiveType(), e.getPrimitiveId());
089        if (p == null) throw e;
090        if (p.isDeleted()) {
091            // we tried to delete an already deleted primitive.
092            Logging.warn(tr("Object ''{0}'' is already deleted on the server. Skipping this object and retrying to upload.",
093                    p.getDisplayName(DefaultNameFormatter.getInstance())));
094            processedPrimitives.addAll(writer.getProcessedPrimitives());
095            processedPrimitives.add(p);
096            toUpload.removeAll(processedPrimitives);
097            return;
098        }
099        // exception was thrown because we tried to *update* an already deleted primitive. We can't resolve this automatically.
100        // Re-throw exception, a conflict is going to be created later.
101        throw e;
102    }
103
104    @Override
105    public void run() {
106        monitor.indeterminateSubTask(tr("Preparing objects to upload ..."));
107        APIDataSet ds = new APIDataSet(layer.getDataSet());
108        try {
109            ds.adjustRelationUploadOrder();
110        } catch (CyclicUploadDependencyException e) {
111            setLastException(e);
112            return;
113        }
114        toUpload = ds.getPrimitives();
115        if (toUpload.isEmpty())
116            return;
117        writer = new OsmServerWriter();
118        try {
119            while (true) {
120                try {
121                    ProgressMonitor m = monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false);
122                    if (isCanceled()) return;
123                    writer.uploadOsm(strategy, toUpload, changeset, m);
124                    processedPrimitives.addAll(writer.getProcessedPrimitives()); // OsmPrimitive in => OsmPrimitive out
125                    break;
126                } catch (OsmApiPrimitiveGoneException e) {
127                    recoverFromGoneOnServer(e);
128                }
129            }
130            if (strategy.isCloseChangesetAfterUpload() && changeset != null && changeset.getId() > 0) {
131                OsmApi.getOsmApi().closeChangeset(changeset, monitor.createSubTaskMonitor(0, false));
132            }
133        } catch (OsmTransferException sxe) {
134            if (isCanceled()) {
135                Logging.info("Ignoring exception caught because upload is canceled. Exception is: " + sxe);
136                return;
137            }
138            setLastException(sxe);
139        }
140
141        if (isCanceled())
142            return;
143        layer.cleanupAfterUpload(processedPrimitives);
144        layer.onPostUploadToServer();
145
146        // don't process exceptions remembered with setLastException().
147        // Caller is supposed to deal with them.
148    }
149
150    @Override
151    public void cancel() {
152        setCanceled(true);
153        if (writer != null) {
154            writer.cancel();
155        }
156    }
157}