source: osm/applications/editors/josm/plugins/reverter/src/reverter/RevertChangesetTask.java@ 36042

Last change on this file since 36042 was 36042, checked in by taylor.smock, 20 months ago

Fix #22520: IllegalStateException: Missing merge target for node (patch by GerdP, modified to add tests)

The added test is essentially an integration test instead of a unit test, mostly
due to having no good way to do a test specifically for the method at issue.
The test uses wiremock to (a) speed up the test and (b) avoid adding a test-time
dependency on the OSM API servers. The test required a custom wiremock response
transformer for the OSM multi-fetch API, since it is not guaranteed that the
order of objects will remain the same, which will lead to stub misses, failing
the test.

File size: 9.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package reverter;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.text.MessageFormat;
7import java.util.ArrayList;
8import java.util.Collection;
9import java.util.Collections;
10import java.util.List;
11import java.util.concurrent.Callable;
12import java.util.stream.Collectors;
13
14import javax.swing.JOptionPane;
15
16import org.openstreetmap.josm.command.Command;
17import org.openstreetmap.josm.command.conflict.ConflictAddCommand;
18import org.openstreetmap.josm.data.UndoRedoHandler;
19import org.openstreetmap.josm.data.osm.DataSet;
20import org.openstreetmap.josm.gui.MainApplication;
21import org.openstreetmap.josm.gui.Notification;
22import org.openstreetmap.josm.gui.PleaseWaitRunnable;
23import org.openstreetmap.josm.gui.layer.OsmDataLayer;
24import org.openstreetmap.josm.gui.progress.ProgressMonitor;
25import org.openstreetmap.josm.gui.util.GuiHelper;
26import org.openstreetmap.josm.io.OsmTransferException;
27import org.openstreetmap.josm.tools.Logging;
28import org.openstreetmap.josm.tools.UserCancelException;
29
30import reverter.ChangesetReverter.RevertType;
31
32public class RevertChangesetTask extends PleaseWaitRunnable {
33 private final Collection<Integer> changesetIds;
34 private final RevertType revertType;
35 private boolean newLayer;
36 private final DataSet oldDataSet;
37
38 private ChangesetReverter rev;
39 private boolean downloadConfirmed;
40 private int numberOfConflicts;
41
42 public RevertChangesetTask(int changesetId, RevertType revertType) {
43 this(changesetId, revertType, false);
44 }
45
46 public RevertChangesetTask(int changesetId, RevertType revertType, boolean autoConfirmDownload) {
47 this(changesetId, revertType, autoConfirmDownload, false);
48 }
49
50 public RevertChangesetTask(int changesetId, RevertType revertType, boolean autoConfirmDownload, boolean newLayer) {
51 this(Collections.singleton(changesetId), revertType, autoConfirmDownload, newLayer);
52 }
53
54 public RevertChangesetTask(Collection<Integer> changesetIds, RevertType revertType, boolean autoConfirmDownload, boolean newLayer) {
55 this(null, changesetIds, revertType, autoConfirmDownload, newLayer);
56 }
57
58 /**
59 * Create a new task for reverting a changeset
60 * @param progressMonitor The {@link ProgressMonitor} to use. May be {@code null}
61 * @param changesetIds The changeset ids to revert
62 * @param revertType The type of revert to do
63 * @param autoConfirmDownload {@code true} if the user has already indicated that they want to download missing data
64 * @param newLayer {@code true} if the user wants the reversion to be on a new layer
65 */
66 public RevertChangesetTask(ProgressMonitor progressMonitor, Collection<Integer> changesetIds, RevertType revertType, boolean autoConfirmDownload, boolean newLayer) {
67 super(tr("Reverting..."), progressMonitor, false);
68 this.changesetIds = new ArrayList<>(changesetIds);
69 this.revertType = revertType;
70 this.downloadConfirmed = autoConfirmDownload;
71 this.newLayer = newLayer;
72 OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
73 this.oldDataSet = editLayer == null ? null : editLayer.data;
74 }
75
76 private boolean checkAndDownloadMissing() throws OsmTransferException {
77 if (!rev.hasMissingObjects()) return true;
78 if (!downloadConfirmed) {
79 final Integer selectedOption = GuiHelper.runInEDTAndWaitAndReturn(new Callable<Integer>() {
80 @Override
81 public Integer call() throws Exception {
82 return JOptionPane.showConfirmDialog(MainApplication.getMainFrame(),
83 tr("This changeset has objects that are not present in current dataset.\n" +
84 "It is needed to download them before reverting. Do you want to continue?"),
85 tr("Confirm"), JOptionPane.YES_NO_OPTION);
86 }
87 });
88 downloadConfirmed = selectedOption != null && selectedOption == JOptionPane.YES_OPTION;
89 if (!downloadConfirmed) return false;
90 }
91 progressMonitor.setTicks(0);
92 rev.downloadMissingPrimitives(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
93 return !progressMonitor.isCanceled();
94 }
95
96 @Override
97 protected void realRun() throws OsmTransferException {
98 numberOfConflicts = 0;
99 final List<Command> allcmds = new ArrayList<>();
100 Logging.info("Reverting {0} changeset(s): {1}",
101 changesetIds.size(), changesetIds.stream().map(Long::toString).collect(Collectors.toList()));
102 for (int changesetId : changesetIds) {
103 try {
104 Logging.info("Reverting changeset {0}", Long.toString(changesetId));
105 RevertChangesetCommand cmd = revertChangeset(changesetId);
106 if (cmd != null) {
107 allcmds.add(cmd);
108 }
109 Logging.info("Reverted changeset {0}", Long.toString(changesetId));
110 newLayer = false; // reuse layer for subsequent reverts
111 } catch (OsmTransferException e) {
112 rollback(allcmds);
113 Logging.error(e);
114 throw e;
115 } catch (UserCancelException e) {
116 rollback(allcmds);
117 GuiHelper.executeByMainWorkerInEDT(() -> new Notification(tr("Revert was canceled")).show());
118 Logging.trace(e);
119 return;
120 }
121 }
122 if (!allcmds.isEmpty()) {
123 Command cmd = allcmds.size() == 1 ? allcmds.get(0) : new RevertChangesetCommand(tr("Revert changesets"), allcmds);
124 GuiHelper.runInEDT(() -> {
125 UndoRedoHandler.getInstance().add(cmd, false);
126 if (numberOfConflicts > 0) {
127 MainApplication.getMap().conflictDialog.warnNumNewConflicts(numberOfConflicts);
128 }
129 });
130 }
131 }
132
133 private static void rollback(List<Command> allcmds) {
134 if (!allcmds.isEmpty()) {
135 GuiHelper.runInEDT(() -> UndoRedoHandler.getInstance().undo(allcmds.size()));
136 }
137 }
138
139 private RevertChangesetCommand revertChangeset(int changesetId) throws OsmTransferException, UserCancelException {
140 progressMonitor.indeterminateSubTask(tr("Reverting changeset {0}", Long.toString(changesetId)));
141 try {
142 rev = new ChangesetReverter(changesetId, revertType, newLayer, oldDataSet, progressMonitor.createSubTaskMonitor(0, true));
143 } catch (final RevertRedactedChangesetException e) {
144 GuiHelper.runInEDT(() -> new Notification(
145 e.getMessage()+"<br>"+
146 tr("See {0}", "<a href=\"https://www.openstreetmap.org/redactions\">https://www.openstreetmap.org/redactions</a>"))
147 .setIcon(JOptionPane.ERROR_MESSAGE)
148 .setDuration(Notification.TIME_LONG)
149 .show());
150 progressMonitor.cancel();
151 }
152 if (progressMonitor.isCanceled())
153 throw new UserCancelException();
154 int numOldConflicts = oldDataSet == null ? 0 : oldDataSet.getConflicts().size();
155 // Check missing objects
156 rev.checkMissingCreated();
157 rev.checkMissingUpdated();
158 if (rev.hasMissingObjects()) {
159 // If missing created or updated objects, ask user
160 rev.checkMissingDeleted();
161 if (!checkAndDownloadMissing())
162 throw new UserCancelException();
163 } else {
164 // Don't ask user to download primitives going to be undeleted
165 rev.checkMissingDeleted();
166 rev.downloadMissingPrimitives(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
167 }
168 int numConflicts = oldDataSet == null ? 0 : oldDataSet.getConflicts().size();
169 if (numConflicts > numOldConflicts) {
170 GuiHelper.runInEDT(() -> new Notification(tr("Please solve conflicts and maybe try again to revert."))
171 .setIcon(JOptionPane.ERROR_MESSAGE)
172 .show());
173 return null;
174 }
175
176 if (progressMonitor.isCanceled())
177 throw new UserCancelException();
178 progressMonitor.setTicks(0);
179 rev.downloadObjectsHistory(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
180 if (progressMonitor.isCanceled())
181 throw new UserCancelException();
182 if (!checkAndDownloadMissing())
183 throw new UserCancelException();
184 progressMonitor.setTicks(0);
185 rev.fixNodesWithoutCoordinates(progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
186 if (progressMonitor.isCanceled())
187 throw new UserCancelException();
188 List<Command> cmds = rev.getCommands();
189 if (cmds.isEmpty()) {
190 Logging.warn(MessageFormat.format("No revert commands found for changeset {0}", Long.toString(changesetId)));
191 return null;
192 }
193 GuiHelper.runInEDT(() -> {
194 for (Command c : cmds) {
195 if (c instanceof ConflictAddCommand) {
196 numberOfConflicts++;
197 }
198 c.executeCommand();
199 }
200 });
201 final String desc;
202 if (revertType == RevertType.FULL) {
203 desc = tr("Revert changeset {0}", String.valueOf(changesetId));
204 } else {
205 desc = tr("Partially revert changeset {0}", String.valueOf(changesetId));
206 }
207 return new RevertChangesetCommand(desc, cmds);
208 }
209
210 @Override
211 protected void cancel() {
212 // nothing to do
213 }
214
215 @Override
216 protected void finish() {
217 // nothing to do
218
219 }
220
221 /**
222 * Return number of conflicts for this changeset.
223 * @return number of conflicts for this changeset
224 */
225 public final int getNumberOfConflicts() {
226 return numberOfConflicts;
227 }
228}
Note: See TracBrowser for help on using the repository browser.