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