source: osm/applications/editors/josm/plugins/reverter/src/reverter/ChangesetReverter.java@ 31590

Last change on this file since 31590 was 31590, checked in by simon04, 9 years ago

JOSM/reverter: Shouldn't display the changeset number with thousands separator in the layer name - fixes #josm11922

File size: 19.4 KB
Line 
1package reverter;
2
3import static org.openstreetmap.josm.tools.I18n.tr;
4
5import java.net.HttpURLConnection;
6import java.util.Arrays;
7import java.util.Collection;
8import java.util.Collections;
9import java.util.HashSet;
10import java.util.Iterator;
11import java.util.List;
12
13import org.openstreetmap.josm.Main;
14import org.openstreetmap.josm.command.Command;
15import org.openstreetmap.josm.command.DeleteCommand;
16import org.openstreetmap.josm.command.conflict.ConflictAddCommand;
17import org.openstreetmap.josm.data.conflict.Conflict;
18import org.openstreetmap.josm.data.coor.LatLon;
19import org.openstreetmap.josm.data.osm.Changeset;
20import org.openstreetmap.josm.data.osm.DataSet;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.osm.OsmPrimitive;
23import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
24import org.openstreetmap.josm.data.osm.PrimitiveId;
25import org.openstreetmap.josm.data.osm.Relation;
26import org.openstreetmap.josm.data.osm.RelationMemberData;
27import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
28import org.openstreetmap.josm.data.osm.Way;
29import org.openstreetmap.josm.data.osm.history.HistoryNode;
30import org.openstreetmap.josm.data.osm.history.HistoryOsmPrimitive;
31import org.openstreetmap.josm.data.osm.history.HistoryRelation;
32import org.openstreetmap.josm.data.osm.history.HistoryWay;
33import org.openstreetmap.josm.gui.layer.OsmDataLayer;
34import org.openstreetmap.josm.gui.progress.ProgressMonitor;
35import org.openstreetmap.josm.gui.util.GuiHelper;
36import org.openstreetmap.josm.io.MultiFetchServerObjectReader;
37import org.openstreetmap.josm.io.OsmApiException;
38import org.openstreetmap.josm.io.OsmTransferException;
39
40import reverter.corehacks.ChangesetDataSet;
41import reverter.corehacks.ChangesetDataSet.ChangesetDataSetEntry;
42import reverter.corehacks.ChangesetDataSet.ChangesetModificationType;
43import reverter.corehacks.OsmServerChangesetReader;
44
45/**
46 * Fetches and stores data for reverting of specific changeset.
47 * @author Upliner
48 *
49 */
50public class ChangesetReverter {
51
52 public static enum RevertType {
53 FULL,
54 SELECTION,
55 SELECTION_WITH_UNDELETE
56 }
57
58 public static final Collection<Long> MODERATOR_REDACTION_ACCOUNTS = Collections.unmodifiableCollection(Arrays.asList(
59 722137L, // OSMF Redaction Account
60 760215L // pnorman redaction revert
61 ));
62
63 public final int changesetId;
64 public final Changeset changeset;
65 public final RevertType revertType;
66
67 private final OsmDataLayer layer; // data layer associated with reverter
68 private final DataSet ds; // DataSet associated with reverter
69 private final ChangesetDataSet cds; // Current changeset data
70 private DataSet nds; // Dataset that contains new objects downloaded by reverter
71
72 private final HashSet<PrimitiveId> missing = new HashSet<>();
73
74 private final HashSet<HistoryOsmPrimitive> created = new HashSet<>();
75 private final HashSet<HistoryOsmPrimitive> updated = new HashSet<>();
76 private final HashSet<HistoryOsmPrimitive> deleted = new HashSet<>();
77
78 //// Handling missing objects
79 ////////////////////////////////////////
80 private void addIfMissing(PrimitiveId id) {
81 OsmPrimitive p = ds.getPrimitiveById(id);
82 if (p == null || p.isIncomplete()) {
83 missing.add(id);
84 }
85 }
86 private void addMissingHistoryIds(Iterable<HistoryOsmPrimitive> primitives) {
87 for (HistoryOsmPrimitive p : primitives) {
88 addIfMissing(p.getPrimitiveId());
89 if (p.getType() == OsmPrimitiveType.WAY) {
90 for (long nd : ((HistoryWay)p).getNodes()) {
91 addIfMissing(new SimplePrimitiveId(nd,OsmPrimitiveType.NODE));
92 }
93 }
94 }
95 }
96
97 private void addMissingIds(Iterable<OsmPrimitive> primitives) {
98 for (OsmPrimitive p : primitives) {
99 addIfMissing(p);
100 if (p.getType() == OsmPrimitiveType.WAY) {
101 for (Node nd : ((Way)p).getNodes()) {
102 addIfMissing(nd);
103 }
104 }
105 }
106 }
107
108 /**
109 * Checks if {@see ChangesetDataSetEntry} conforms to current RevertType
110 * @param entry entry to be checked
111 * @return <code>true</code> if {@see ChangesetDataSetEntry} conforms to current RevertType
112 */
113 private boolean checkOsmChangeEntry(ChangesetDataSetEntry entry) {
114 if (revertType == RevertType.FULL) return true;
115 if (revertType == RevertType.SELECTION_WITH_UNDELETE &&
116 entry.getModificationType() == ChangesetModificationType.DELETED) {
117 return true;
118 }
119 OsmPrimitive p = ds.getPrimitiveById(entry.getPrimitive().getPrimitiveId());
120 if (p == null) return false;
121 return p.isSelected();
122 }
123
124 /**
125 * creates a reverter for specific changeset and fetches initial data
126 * @param changesetId
127 * @param monitor
128 * @throws OsmTransferException
129 * @throws RevertRedactedChangesetException
130 */
131 public ChangesetReverter(int changesetId, RevertType revertType, boolean newLayer, ProgressMonitor monitor)
132 throws OsmTransferException, RevertRedactedChangesetException {
133 this.changesetId = changesetId;
134 if (newLayer) {
135 this.ds = new DataSet();
136 this.layer = new OsmDataLayer(this.ds, tr("Reverted changeset") + tr(" [id: {0}]", String.valueOf(changesetId)), null);
137 } else {
138 this.layer = Main.main.getEditLayer();
139 this.ds = layer.data;
140 }
141 this.revertType = revertType;
142
143 OsmServerChangesetReader csr = new OsmServerChangesetReader();
144 monitor.beginTask("", 2);
145 changeset = csr.readChangeset(changesetId, monitor.createSubTaskMonitor(1, false));
146 if (MODERATOR_REDACTION_ACCOUNTS.contains(changeset.getUser().getId())) {
147 throw new RevertRedactedChangesetException(tr("It is not allowed to revert changeset from {0}", changeset.getUser().getName()));
148 }
149 try {
150 cds = csr.downloadChangeset(changesetId, monitor.createSubTaskMonitor(1, false));
151 } finally {
152 monitor.finishTask();
153 if (newLayer) {
154 GuiHelper.runInEDT(new Runnable() {
155 @Override
156 public void run() {
157 Main.main.addLayer(layer);
158 }
159 });
160 }
161 }
162
163 // Build our own lists of created/updated/modified objects for better performance
164 for (Iterator<ChangesetDataSetEntry> it = cds.iterator();it.hasNext();) {
165 ChangesetDataSetEntry entry = it.next();
166 if (!checkOsmChangeEntry(entry)) continue;
167 if (entry.getModificationType() == ChangesetModificationType.CREATED) {
168 created.add(entry.getPrimitive());
169 } else if (entry.getModificationType() == ChangesetModificationType.UPDATED) {
170 updated.add(entry.getPrimitive());
171 } else if (entry.getModificationType() == ChangesetModificationType.DELETED) {
172 deleted.add(entry.getPrimitive());
173 } else throw new AssertionError();
174 }
175 }
176 public void checkMissingCreated() {
177 addMissingHistoryIds(created);
178 }
179 public void checkMissingUpdated() {
180 addMissingHistoryIds(updated);
181 }
182 public void checkMissingDeleted() {
183 addMissingHistoryIds(deleted);
184 }
185
186 private void readObjectVersion(OsmServerMultiObjectReader rdr, PrimitiveId id, int version, ProgressMonitor progressMonitor) throws OsmTransferException {
187 boolean readOK = false;
188 while (!readOK && version >= 1) {
189 try {
190 rdr.readObject(id, version, progressMonitor.createSubTaskMonitor(1, true));
191 readOK = true;
192 } catch (OsmApiException e) {
193 if (e.getResponseCode() != HttpURLConnection.HTTP_FORBIDDEN) {
194 throw e;
195 }
196 String message = "Version "+version+" of "+id+" is unauthorized";
197 if (version > 1) {
198 message += ", requesting previous one";
199 }
200 Main.info(message);
201 version--;
202 }
203 }
204 if (!readOK) {
205 Main.warn("Cannot retrieve any previous version of "+id);
206 }
207 }
208
209 /**
210 * fetch objects that were updated or deleted by changeset
211 * @param progressMonitor
212 * @throws OsmTransferException
213 */
214 @SuppressWarnings("unchecked")
215 public void downloadObjectsHistory(ProgressMonitor progressMonitor) throws OsmTransferException {
216 final OsmServerMultiObjectReader rdr = new OsmServerMultiObjectReader();
217
218 progressMonitor.beginTask(tr("Downloading objects history"),updated.size()+deleted.size()+1);
219 try {
220 for (HashSet<HistoryOsmPrimitive> collection : Arrays.asList(new HashSet[]{updated, deleted})) {
221 for (HistoryOsmPrimitive entry : collection) {
222 PrimitiveId id = entry.getPrimitiveId();
223 readObjectVersion(rdr, id, cds.getEarliestVersion(id)-1, progressMonitor);
224 if (progressMonitor.isCanceled()) return;
225 }
226 }
227 nds = rdr.parseOsm(progressMonitor.createSubTaskMonitor(1, true));
228 for (OsmPrimitive p : nds.allPrimitives()) {
229 if (!p.isIncomplete()) {
230 addMissingIds(Collections.singleton(p));
231 } else {
232 if (ds.getPrimitiveById(p.getPrimitiveId()) == null) {
233 switch (p.getType()) {
234 case NODE: ds.addPrimitive(new Node(p.getUniqueId())); break;
235 case CLOSEDWAY:
236 case WAY: ds.addPrimitive(new Way(p.getUniqueId())); break;
237 case MULTIPOLYGON:
238 case RELATION: ds.addPrimitive(new Relation(p.getUniqueId())); break;
239 default: throw new AssertionError();
240 }
241 }
242 }
243 }
244 } finally {
245 progressMonitor.finishTask();
246 }
247 }
248
249 public void downloadMissingPrimitives(ProgressMonitor monitor) throws OsmTransferException {
250 if (!hasMissingObjects()) return;
251 MultiFetchServerObjectReader rdr = new MultiFetchServerObjectReader();
252 for (PrimitiveId id : missing) {
253 switch (id.getType()) {
254 case NODE:
255 rdr.append(new Node(id.getUniqueId()));
256 break;
257 case CLOSEDWAY:
258 case WAY:
259 rdr.append(new Way(id.getUniqueId()));
260 break;
261 case MULTIPOLYGON:
262 case RELATION:
263 rdr.append(new Relation(id.getUniqueId()));
264 break;
265 default: throw new AssertionError();
266 }
267 }
268 DataSet source = rdr.parseOsm(monitor);
269 for (OsmPrimitive p : source.allPrimitives()) {
270 if (!p.isVisible() && !p.isDeleted()) {
271 p.setDeleted(true);
272 p.setModified(false);
273 }
274 }
275 layer.mergeFrom(source);
276 missing.clear();
277 }
278
279 private static Conflict<? extends OsmPrimitive> CreateConflict(OsmPrimitive p, boolean isMyDeleted) {
280 switch (p.getType()) {
281 case NODE:
282 return new Conflict<>((Node)p,new Node((Node)p), isMyDeleted);
283 case CLOSEDWAY:
284 case WAY:
285 return new Conflict<>((Way)p,new Way((Way)p), isMyDeleted);
286 case MULTIPOLYGON:
287 case RELATION:
288 return new Conflict<>((Relation)p,new Relation((Relation)p), isMyDeleted);
289 default: throw new AssertionError();
290 }
291 }
292
293 private boolean hasEqualSemanticAttributes(OsmPrimitive current,HistoryOsmPrimitive history) {
294 if (!current.getKeys().equals(history.getTags())) return false;
295 switch (current.getType()) {
296 case NODE:
297 LatLon currentCoor = ((Node)current).getCoor();
298 LatLon historyCoor = ((HistoryNode)history).getCoords();
299 if (currentCoor == historyCoor || (currentCoor != null && historyCoor != null && currentCoor.equals(historyCoor)))
300 return true;
301 // Handle case where a deleted note has been restored to avoid false conflicts (fix #josm8660)
302 if (currentCoor != null && historyCoor == null) {
303 LatLon previousCoor = ((Node)nds.getPrimitiveById(history.getPrimitiveId())).getCoor();
304 return previousCoor != null && previousCoor.equals(currentCoor);
305 }
306 return false;
307 case CLOSEDWAY:
308 case WAY:
309 List<Node> currentNodes = ((Way)current).getNodes();
310 List<Long> historyNodes = ((HistoryWay)history).getNodes();
311 if (currentNodes.size() != historyNodes.size()) return false;
312 for (int i = 0; i < currentNodes.size(); i++) {
313 if (currentNodes.get(i).getId() != historyNodes.get(i)) return false;
314 }
315 return true;
316 case MULTIPOLYGON:
317 case RELATION:
318 List<org.openstreetmap.josm.data.osm.RelationMember> currentMembers =
319 ((Relation)current).getMembers();
320 List<RelationMemberData> historyMembers = ((HistoryRelation)history).getMembers();
321 if (currentMembers.size() != historyMembers.size()) return false;
322 for (int i = 0; i < currentMembers.size(); i++) {
323 org.openstreetmap.josm.data.osm.RelationMember currentMember =
324 currentMembers.get(i);
325 RelationMemberData historyMember = historyMembers.get(i);
326 if (!currentMember.getRole().equals(historyMember.getRole())) return false;
327 if (!currentMember.getMember().getPrimitiveId().equals(new SimplePrimitiveId(
328 historyMember.getMemberId(),historyMember.getMemberType()))) return false;
329 }
330 return true;
331 default: throw new AssertionError();
332 }
333 }
334
335 /**
336 * Builds a list of commands that will revert the changeset
337 *
338 */
339 public List<Command> getCommands() {
340 if (this.nds == null) return null;
341
342 //////////////////////////////////////////////////////////////////////////
343 // Create commands to restore/update all affected objects
344 DataSetCommandMerger merger = new DataSetCommandMerger(nds,ds);
345 List<Command> cmds = merger.getCommandList();
346
347 //////////////////////////////////////////////////////////////////////////
348 // Create a set of objects to be deleted
349
350 HashSet<OsmPrimitive> toDelete = new HashSet<>();
351 // Mark objects that has visible=false to be deleted
352 for (OsmPrimitive p : nds.allPrimitives()) {
353 if (!p.isVisible()) {
354 OsmPrimitive dp = ds.getPrimitiveById(p);
355 if (dp != null) toDelete.add(dp);
356 }
357 }
358 // Mark all created objects to be deleted
359 for (HistoryOsmPrimitive id : created) {
360 OsmPrimitive p = ds.getPrimitiveById(id.getPrimitiveId());
361 if (p != null) toDelete.add(p);
362 }
363
364 //////////////////////////////////////////////////////////////////////////
365 // Check reversion against current dataset and create necessary conflicts
366
367 HashSet<OsmPrimitive> conflicted = new HashSet<>();
368
369 for (Conflict<? extends OsmPrimitive> conflict : merger.getConflicts()) {
370 cmds.add(new ConflictAddCommand(layer,conflict));
371 }
372
373 // Check objects versions
374 for (Iterator<ChangesetDataSetEntry> it = cds.iterator();it.hasNext();) {
375 ChangesetDataSetEntry entry = it.next();
376 if (!checkOsmChangeEntry(entry)) continue;
377 HistoryOsmPrimitive hp = entry.getPrimitive();
378 OsmPrimitive dp = ds.getPrimitiveById(hp.getPrimitiveId());
379 if (dp == null || dp.isIncomplete())
380 throw new IllegalStateException(tr("Missing merge target for {0} with id {1}",
381 hp.getType(), hp.getId()));
382
383 if (hp.getVersion() != dp.getVersion()
384 && (hp.isVisible() || dp.isVisible()) &&
385 /* Don't create conflict if changeset object and dataset object
386 * has same semantic attributes (but different versions) */
387 !hasEqualSemanticAttributes(dp,hp)
388 /* Don't create conflict if the object has to be deleted but has already been deleted */
389 && !(toDelete.contains(dp) && dp.isDeleted())) {
390 cmds.add(new ConflictAddCommand(layer,CreateConflict(dp,
391 entry.getModificationType() == ChangesetModificationType.CREATED)));
392 conflicted.add(dp);
393 }
394 }
395
396 /* Check referrers for deleted objects: if object is referred by another object that
397 * isn't going to be deleted or modified, create a conflict.
398 */
399 for (Iterator<OsmPrimitive> it = toDelete.iterator(); it.hasNext();) {
400 OsmPrimitive p = it.next();
401 if (p.isDeleted()) {
402 it.remove();
403 continue;
404 }
405 for (OsmPrimitive referrer : p.getReferrers()) {
406 if (toDelete.contains(referrer)) continue; // object is going to be deleted
407 if (nds.getPrimitiveById(referrer) != null)
408 continue; /* object is going to be modified so it cannot refer to
409 * objects created in changeset to be reverted
410 */
411 if (!conflicted.contains(p)) {
412 cmds.add(new ConflictAddCommand(layer,CreateConflict(p, true)));
413 conflicted.add(p);
414 }
415 it.remove();
416 break;
417 }
418 }
419
420 // Create a Command to delete all marked objects
421 List<? extends OsmPrimitive> list;
422 list = OsmPrimitive.getFilteredList(toDelete, Relation.class);
423 if (!list.isEmpty()) cmds.add(new DeleteCommand(list));
424 list = OsmPrimitive.getFilteredList(toDelete, Way.class);
425 if (!list.isEmpty()) cmds.add(new DeleteCommand(list));
426 list = OsmPrimitive.getFilteredList(toDelete, Node.class);
427 if (!list.isEmpty()) cmds.add(new DeleteCommand(list));
428 return cmds;
429 }
430
431 public boolean hasMissingObjects() {
432 return !missing.isEmpty();
433 }
434
435 public void fixNodesWithoutCoordinates(ProgressMonitor progressMonitor) throws OsmTransferException {
436 for (Node n : nds.getNodes()) {
437 if (!n.isDeleted() && n.getCoor() == null) {
438 PrimitiveId id = n.getPrimitiveId();
439 OsmPrimitive p = ds.getPrimitiveById(id);
440 if (p instanceof Node && p.getVersion() > 1) {
441 LatLon coor = ((Node)p).getCoor();
442 if (coor == null) {
443 final OsmServerMultiObjectReader rdr = new OsmServerMultiObjectReader();
444 readObjectVersion(rdr, id, p.getVersion()-1, progressMonitor);
445 Collection<OsmPrimitive> result = rdr.parseOsm(progressMonitor.createSubTaskMonitor(1, true)).allPrimitives();
446 if (!result.isEmpty()) {
447 coor = ((Node)result.iterator().next()).getCoor();
448 }
449 }
450 if (coor != null) {
451 n.setCoor(coor);
452 }
453 }
454 }
455 }
456 }
457}
Note: See TracBrowser for help on using the repository browser.