001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.history;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.awt.Component;
008import java.io.IOException;
009import java.net.HttpURLConnection;
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.LinkedHashSet;
013import java.util.List;
014import java.util.Objects;
015import java.util.Set;
016
017import org.openstreetmap.josm.data.osm.Changeset;
018import org.openstreetmap.josm.data.osm.OsmPrimitive;
019import org.openstreetmap.josm.data.osm.PrimitiveId;
020import org.openstreetmap.josm.data.osm.history.History;
021import org.openstreetmap.josm.data.osm.history.HistoryDataSet;
022import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
023import org.openstreetmap.josm.gui.ExceptionDialogUtil;
024import org.openstreetmap.josm.gui.PleaseWaitRunnable;
025import org.openstreetmap.josm.gui.progress.ProgressMonitor;
026import org.openstreetmap.josm.io.ChangesetQuery;
027import org.openstreetmap.josm.io.OsmApiException;
028import org.openstreetmap.josm.io.OsmServerChangesetReader;
029import org.openstreetmap.josm.io.OsmServerHistoryReader;
030import org.openstreetmap.josm.io.OsmTransferException;
031import org.openstreetmap.josm.tools.CheckParameterUtil;
032import org.xml.sax.SAXException;
033
034/**
035 * Loads the object history of a collection of objects from the server.
036 *
037 * It provides a fluent API for configuration.
038 *
039 * Sample usage:
040 *
041 * <pre>
042 *   HistoryLoadTask task = new HistoryLoadTask()
043 *      .add(node)
044 *      .add(way)
045 *      .add(relation)
046 *      .add(aHistoryItem);
047 *
048 *   MainApplication.worker.execute(task);
049 * </pre>
050 */
051public class HistoryLoadTask extends PleaseWaitRunnable {
052
053    private boolean canceled;
054    private Exception lastException;
055    private final Set<PrimitiveId> toLoad = new LinkedHashSet<>();
056    private HistoryDataSet loadedData;
057    private OsmServerHistoryReader reader;
058    private boolean getChangesetData = true;
059    private boolean collectMissing;
060    private final Set<PrimitiveId> missingPrimitives = new LinkedHashSet<>();
061
062    /**
063     * Constructs a new {@code HistoryLoadTask}.
064     */
065    public HistoryLoadTask() {
066        super(tr("Load history"), true);
067    }
068
069    /**
070     * Constructs a new {@code HistoryLoadTask}.
071     *
072     * @param parent the component to be used as reference to find the
073     * parent for {@link org.openstreetmap.josm.gui.PleaseWaitDialog}.
074     * Must not be <code>null</code>.
075     * @throws NullPointerException if parent is <code>null</code>
076     */
077    public HistoryLoadTask(Component parent) {
078        super(Objects.requireNonNull(parent, "parent"), tr("Load history"), true);
079    }
080
081    /**
082     * Adds an object whose history is to be loaded.
083     *
084     * @param pid  the primitive id. Must not be null. Id &gt; 0 required.
085     * @return this task
086     */
087    public HistoryLoadTask add(PrimitiveId pid) {
088        CheckParameterUtil.ensureThat(pid.getUniqueId() > 0, "id > 0");
089        toLoad.add(pid);
090        return this;
091    }
092
093    /**
094     * Adds an object to be loaded, the object is specified by a history item.
095     *
096     * @param primitive the history item
097     * @return this task
098     * @throws NullPointerException if primitive is null
099     */
100    public HistoryLoadTask add(HistoryOsmPrimitive primitive) {
101        return add(primitive.getPrimitiveId());
102    }
103
104    /**
105     * Adds an object to be loaded, the object is specified by an already loaded object history.
106     *
107     * @param history the history. Must not be null.
108     * @return this task
109     * @throws NullPointerException if history is null
110     */
111    public HistoryLoadTask add(History history) {
112        return add(history.getPrimitiveId());
113    }
114
115    /**
116     * Adds an object to be loaded, the object is specified by an OSM primitive.
117     *
118     * @param primitive the OSM primitive. Must not be null. primitive.getOsmId() &gt; 0 required.
119     * @return this task
120     * @throws NullPointerException if the primitive is null
121     * @throws IllegalArgumentException if primitive.getOsmId() &lt;= 0
122     */
123    public HistoryLoadTask add(OsmPrimitive primitive) {
124        CheckParameterUtil.ensureThat(primitive.getOsmId() > 0, "id > 0");
125        return add(primitive.getOsmPrimitiveId());
126    }
127
128    /**
129     * Adds a collection of objects to loaded, specified by a collection of OSM primitives.
130     *
131     * @param primitives the OSM primitives. Must not be <code>null</code>.
132     * <code>primitive.getId() &gt; 0</code> required.
133     * @return this task
134     * @throws NullPointerException if primitives is null
135     * @throws IllegalArgumentException if one of the ids in the collection &lt;= 0
136     * @since 16123
137     */
138    public HistoryLoadTask addPrimitiveIds(Collection<? extends PrimitiveId> primitives) {
139        primitives.forEach(this::add);
140        return this;
141    }
142
143    /**
144     * Adds a collection of objects to loaded, specified by a collection of OSM primitives.
145     *
146     * @param primitives the OSM primitives. Must not be <code>null</code>.
147     * <code>primitive.getId() &gt; 0</code> required.
148     * @return this task
149     * @throws NullPointerException if primitives is null
150     * @throws IllegalArgumentException if one of the ids in the collection &lt;= 0
151     * @since 16123
152     */
153    public HistoryLoadTask addOsmPrimitives(Collection<? extends OsmPrimitive> primitives) {
154        primitives.forEach(this::add);
155        return this;
156    }
157
158    @Override
159    protected void cancel() {
160        if (reader != null) {
161            reader.cancel();
162        }
163        canceled = true;
164    }
165
166    @Override
167    protected void finish() {
168        if (isCanceled())
169            return;
170        if (lastException != null) {
171            ExceptionDialogUtil.explainException(lastException);
172            return;
173        }
174        HistoryDataSet.getInstance().mergeInto(loadedData);
175    }
176
177    @Override
178    protected void realRun() throws SAXException, IOException, OsmTransferException {
179        loadedData = new HistoryDataSet();
180        int ticks = toLoad.size();
181        if (getChangesetData)
182            ticks *= 2;
183        try {
184            progressMonitor.setTicksCount(ticks);
185            for (PrimitiveId pid: toLoad) {
186                if (canceled) {
187                    break;
188                }
189                loadHistory(pid);
190            }
191        } catch (OsmTransferException e) {
192            lastException = e;
193        }
194    }
195
196    private void loadHistory(PrimitiveId pid) throws OsmTransferException {
197        String msg = getLoadingMessage(pid);
198        progressMonitor.indeterminateSubTask(tr(msg, Long.toString(pid.getUniqueId())));
199        reader = null;
200        HistoryDataSet ds = null;
201        try {
202            reader = new OsmServerHistoryReader(pid.getType(), pid.getUniqueId());
203            if (getChangesetData) {
204                ds = loadHistory(reader, progressMonitor);
205            } else {
206                ds = reader.parseHistory(progressMonitor.createSubTaskMonitor(1, false));
207            }
208        } catch (OsmApiException e) {
209            if (canceled)
210                return;
211            if (e.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND && collectMissing) {
212                missingPrimitives.add(pid);
213            } else {
214                throw e;
215            }
216        } catch (OsmTransferException e) {
217            if (canceled)
218                return;
219            throw e;
220        }
221        if (ds != null) {
222            loadedData.mergeInto(ds);
223        }
224    }
225
226    protected static HistoryDataSet loadHistory(OsmServerHistoryReader reader, ProgressMonitor progressMonitor) throws OsmTransferException {
227        HistoryDataSet ds = reader.parseHistory(progressMonitor.createSubTaskMonitor(1, false));
228        if (ds != null) {
229            // load corresponding changesets (mostly for changeset comment)
230            OsmServerChangesetReader changesetReader = new OsmServerChangesetReader();
231            List<Long> changesetIds = new ArrayList<>(ds.getChangesetIds());
232
233            // query changesets 100 by 100 (OSM API limit)
234            int n = ChangesetQuery.MAX_CHANGESETS_NUMBER;
235            for (int i = 0; i < changesetIds.size(); i += n) {
236                for (Changeset c : changesetReader.queryChangesets(
237                        new ChangesetQuery().forChangesetIds(changesetIds.subList(i, Math.min(i + n, changesetIds.size()))),
238                        progressMonitor.createSubTaskMonitor(1, false))) {
239                    ds.putChangeset(c);
240                }
241            }
242        }
243        return ds;
244    }
245
246    protected static String getLoadingMessage(PrimitiveId pid) {
247        switch (pid.getType()) {
248        case NODE:
249            return marktr("Loading history for node {0}");
250        case WAY:
251            return marktr("Loading history for way {0}");
252        case RELATION:
253            return marktr("Loading history for relation {0}");
254        default:
255            return "";
256        }
257    }
258
259    /**
260     * Determines if this task has ben canceled.
261     * @return {@code true} if this task has ben canceled
262     */
263    public boolean isCanceled() {
264        return canceled;
265    }
266
267    /**
268     * Returns the last exception that occurred during loading, if any.
269     * @return the last exception that occurred during loading, or {@code null}
270     */
271    public Exception getLastException() {
272        return lastException;
273    }
274
275    /**
276     * Determine if changeset information is needed. By default it is retrieved.
277     * @param b false means don't retrieve changeset data.
278     * @since 14763
279     */
280    public void setChangesetDataNeeded(boolean b) {
281        getChangesetData = b;
282    }
283
284    /**
285     * Determine if missing primitives should be collected. By default they are not collected
286     * and the first missing object terminates the task.
287     * @param b true means collect missing data and continue.
288     * @since 16205
289     */
290    public void setCollectMissing(boolean b) {
291        collectMissing = b;
292    }
293
294    /**
295     * replies the set of ids of all primitives for which a fetch request to the
296     * server was submitted but which are not available from the server (the server
297     * replied a return code of 404)
298     * @return the set of ids of missing primitives
299     * @since 16205
300     */
301    public Set<PrimitiveId> getMissingPrimitives() {
302        return missingPrimitives;
303    }
304
305}