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 > 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() > 0 required. 119 * @return this task 120 * @throws NullPointerException if the primitive is null 121 * @throws IllegalArgumentException if primitive.getOsmId() <= 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() > 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 <= 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() > 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 <= 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}