source: josm/trunk/src/org/openstreetmap/josm/gui/io/AbstractUploadTask.java@ 16407

Last change on this file since 16407 was 16407, checked in by GerdP, 4 years ago

fix #18915: "Precondition violation" not fixable from within JOSM

  • correct handling of precondition failure responses that contain the word requires so that the correct objects are downloaded. The parents are needed, not the childern.
  • Property svn:eol-style set to native
File size: 15.8 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.io;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.event.ActionEvent;
8import java.net.HttpURLConnection;
9import java.text.DateFormat;
10import java.util.ArrayList;
11import java.util.Arrays;
12import java.util.Collection;
13import java.util.Collections;
14import java.util.Date;
15import java.util.regex.Matcher;
16import java.util.regex.Pattern;
17
18import javax.swing.JOptionPane;
19
20import org.openstreetmap.josm.actions.DownloadReferrersAction;
21import org.openstreetmap.josm.actions.UpdateDataAction;
22import org.openstreetmap.josm.actions.UpdateSelectionAction;
23import org.openstreetmap.josm.data.osm.OsmPrimitive;
24import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
25import org.openstreetmap.josm.gui.ExceptionDialogUtil;
26import org.openstreetmap.josm.gui.HelpAwareOptionPane;
27import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
28import org.openstreetmap.josm.gui.MainApplication;
29import org.openstreetmap.josm.gui.PleaseWaitRunnable;
30import org.openstreetmap.josm.gui.layer.OsmDataLayer;
31import org.openstreetmap.josm.gui.progress.ProgressMonitor;
32import org.openstreetmap.josm.io.OsmApiException;
33import org.openstreetmap.josm.io.OsmApiInitializationException;
34import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
35import org.openstreetmap.josm.tools.ExceptionUtil;
36import org.openstreetmap.josm.tools.ImageProvider;
37import org.openstreetmap.josm.tools.Logging;
38import org.openstreetmap.josm.tools.Pair;
39import org.openstreetmap.josm.tools.date.DateUtils;
40
41/**
42 * Abstract base class for the task of uploading primitives via OSM API.
43 *
44 * Mainly handles conflicts and certain error situations.
45 */
46public abstract class AbstractUploadTask extends PleaseWaitRunnable {
47
48 /**
49 * Constructs a new {@code AbstractUploadTask}.
50 * @param title message for the user
51 * @param ignoreException If true, exception will be silently ignored. If false then
52 * exception will be handled by showing a dialog. When this runnable is executed using executor framework
53 * then use false unless you read result of task (because exception will get lost if you don't)
54 */
55 public AbstractUploadTask(String title, boolean ignoreException) {
56 super(title, ignoreException);
57 }
58
59 /**
60 * Constructs a new {@code AbstractUploadTask}.
61 * @param title message for the user
62 * @param progressMonitor progress monitor
63 * @param ignoreException If true, exception will be silently ignored. If false then
64 * exception will be handled by showing a dialog. When this runnable is executed using executor framework
65 * then use false unless you read result of task (because exception will get lost if you don't)
66 */
67 public AbstractUploadTask(String title, ProgressMonitor progressMonitor, boolean ignoreException) {
68 super(title, progressMonitor, ignoreException);
69 }
70
71 /**
72 * Constructs a new {@code AbstractUploadTask}.
73 * @param title message for the user
74 */
75 public AbstractUploadTask(String title) {
76 super(title);
77 }
78
79 /**
80 * Synchronizes the local state of an {@link OsmPrimitive} with its state on the
81 * server. The method uses an individual GET for the primitive.
82 * @param type the primitive type
83 * @param id the primitive ID
84 */
85 protected void synchronizePrimitive(final OsmPrimitiveType type, final long id) {
86 // FIXME: should now about the layer this task is running for. might
87 // be different from the current edit layer
88 OsmDataLayer layer = MainApplication.getLayerManager().getEditLayer();
89 if (layer == null)
90 throw new IllegalStateException(tr("Failed to update primitive with id {0} because current edit layer is null", id));
91 OsmPrimitive p = layer.data.getPrimitiveById(id, type);
92 if (p == null)
93 throw new IllegalStateException(
94 tr("Failed to update primitive with id {0} because current edit layer does not include such a primitive", id));
95 MainApplication.worker.execute(new UpdatePrimitivesTask(layer, Collections.singleton(p)));
96 }
97
98 /**
99 * Synchronizes the local state of the dataset with the state on the server.
100 *
101 * Reuses the functionality of {@link UpdateDataAction}.
102 *
103 * @see UpdateDataAction#actionPerformed(ActionEvent)
104 */
105 protected void synchronizeDataSet() {
106 UpdateDataAction act = new UpdateDataAction();
107 act.actionPerformed(new ActionEvent(this, 0, ""));
108 }
109
110 /**
111 * Handles the case that a conflict in a specific {@link OsmPrimitive} was detected while
112 * uploading
113 *
114 * @param primitiveType the type of the primitive, either <code>node</code>, <code>way</code> or
115 * <code>relation</code>
116 * @param id the id of the primitive
117 * @param serverVersion the version of the primitive on the server
118 * @param myVersion the version of the primitive in the local dataset
119 */
120 protected void handleUploadConflictForKnownConflict(final OsmPrimitiveType primitiveType, final long id, String serverVersion,
121 String myVersion) {
122 String lbl;
123 switch(primitiveType) {
124 // CHECKSTYLE.OFF: SingleSpaceSeparator
125 case NODE: lbl = tr("Synchronize node {0} only", id); break;
126 case WAY: lbl = tr("Synchronize way {0} only", id); break;
127 case RELATION: lbl = tr("Synchronize relation {0} only", id); break;
128 // CHECKSTYLE.ON: SingleSpaceSeparator
129 default: throw new AssertionError();
130 }
131 ButtonSpec[] spec = {
132 new ButtonSpec(
133 lbl,
134 new ImageProvider("updatedata"),
135 null, null),
136 new ButtonSpec(
137 tr("Synchronize entire dataset"),
138 new ImageProvider("updatedata"),
139 null, null),
140 new ButtonSpec(
141 tr("Cancel"),
142 new ImageProvider("cancel"),
143 null, null)
144 };
145 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
146 + "of your nodes, ways, or relations.<br>"
147 + "The conflict is caused by the <strong>{0}</strong> with id <strong>{1}</strong>,<br>"
148 + "the server has version {2}, your version is {3}.<br>"
149 + "<br>"
150 + "Click <strong>{4}</strong> to synchronize the conflicting primitive only.<br>"
151 + "Click <strong>{5}</strong> to synchronize the entire local dataset with the server.<br>"
152 + "Click <strong>{6}</strong> to abort and continue editing.<br></html>",
153 tr(primitiveType.getAPIName()), id, serverVersion, myVersion,
154 spec[0].text, spec[1].text, spec[2].text
155 );
156 int ret = HelpAwareOptionPane.showOptionDialog(
157 MainApplication.getMainFrame(),
158 msg,
159 tr("Conflicts detected"),
160 JOptionPane.ERROR_MESSAGE,
161 null,
162 spec,
163 spec[0],
164 "/Concepts/Conflict"
165 );
166 switch(ret) {
167 case 0: synchronizePrimitive(primitiveType, id); break;
168 case 1: synchronizeDataSet(); break;
169 default: return;
170 }
171 }
172
173 /**
174 * Handles the case that a conflict was detected while uploading where we don't
175 * know what {@link OsmPrimitive} actually caused the conflict (for whatever reason)
176 *
177 */
178 protected void handleUploadConflictForUnknownConflict() {
179 ButtonSpec[] spec = {
180 new ButtonSpec(
181 tr("Synchronize entire dataset"),
182 new ImageProvider("updatedata"),
183 null, null),
184 new ButtonSpec(
185 tr("Cancel"),
186 new ImageProvider("cancel"),
187 null, null)
188 };
189 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
190 + "of your nodes, ways, or relations.<br>"
191 + "<br>"
192 + "Click <strong>{0}</strong> to synchronize the entire local dataset with the server.<br>"
193 + "Click <strong>{1}</strong> to abort and continue editing.<br></html>",
194 spec[0].text, spec[1].text
195 );
196 int ret = HelpAwareOptionPane.showOptionDialog(
197 MainApplication.getMainFrame(),
198 msg,
199 tr("Conflicts detected"),
200 JOptionPane.ERROR_MESSAGE,
201 null,
202 spec,
203 spec[0],
204 ht("/Concepts/Conflict")
205 );
206 if (ret == 0) {
207 synchronizeDataSet();
208 }
209 }
210
211 /**
212 * Handles the case that a conflict was detected while uploading where we don't
213 * know what {@link OsmPrimitive} actually caused the conflict (for whatever reason)
214 * @param changesetId changeset ID
215 * @param d changeset date
216 */
217 protected void handleUploadConflictForClosedChangeset(long changesetId, Date d) {
218 String msg = tr("<html>Uploading <strong>failed</strong> because you have been using<br>"
219 + "changeset {0} which was already closed at {1}.<br>"
220 + "Please upload again with a new or an existing open changeset.</html>",
221 changesetId, DateUtils.formatDateTime(d, DateFormat.SHORT, DateFormat.SHORT)
222 );
223 JOptionPane.showMessageDialog(
224 MainApplication.getMainFrame(),
225 msg,
226 tr("Changeset closed"),
227 JOptionPane.ERROR_MESSAGE
228 );
229 }
230
231 /**
232 * Handles the case where deleting a node failed because it is still in use in
233 * a non-deleted way on the server.
234 * @param e exception
235 * @param conflict conflict
236 */
237 protected void handleUploadPreconditionFailedConflict(OsmApiException e, Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict) {
238 ButtonSpec[] options = {
239 new ButtonSpec(
240 tr("Prepare conflict resolution"),
241 new ImageProvider("ok"),
242 tr("Click to download all referring objects for {0}", conflict.a),
243 null /* no specific help context */
244 ),
245 new ButtonSpec(
246 tr("Cancel"),
247 new ImageProvider("cancel"),
248 tr("Click to cancel and to resume editing the map"),
249 null /* no specific help context */
250 )
251 };
252 String msg = ExceptionUtil.explainPreconditionFailed(e).replace("</html>", "<br><br>" + tr(
253 "Click <strong>{0}</strong> to load them now.<br>"
254 + "If necessary JOSM will create conflicts which you can resolve in the Conflict Resolution Dialog.",
255 options[0].text)) + "</html>";
256 int ret = HelpAwareOptionPane.showOptionDialog(
257 MainApplication.getMainFrame(),
258 msg,
259 tr("Object still in use"),
260 JOptionPane.ERROR_MESSAGE,
261 null,
262 options,
263 options[0],
264 "/Action/Upload#NodeStillInUseInWay"
265 );
266 if (ret == 0) {
267 if (msg.contains("to delete")) {
268 DownloadReferrersAction.downloadReferrers(MainApplication.getLayerManager().getEditLayer(),
269 Arrays.asList(conflict.a));
270 }
271 if (msg.contains("to upload") && !conflict.b.isEmpty()) {
272 MainApplication.worker.submit(new DownloadPrimitivesTask(
273 MainApplication.getLayerManager().getEditLayer(), new ArrayList<>(conflict.b), false));
274 }
275 }
276 }
277
278 /**
279 * handles an upload conflict, i.e. an error indicated by a HTTP return code 409.
280 *
281 * @param e the exception
282 */
283 protected void handleUploadConflict(OsmApiException e) {
284 final String errorHeader = e.getErrorHeader();
285 if (errorHeader != null) {
286 Pattern p = Pattern.compile("Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)");
287 Matcher m = p.matcher(errorHeader);
288 if (m.matches()) {
289 handleUploadConflictForKnownConflict(OsmPrimitiveType.from(m.group(3)), Long.parseLong(m.group(4)), m.group(2), m.group(1));
290 return;
291 }
292 p = Pattern.compile("The changeset (\\d+) was closed at (.*)");
293 m = p.matcher(errorHeader);
294 if (m.matches()) {
295 handleUploadConflictForClosedChangeset(Long.parseLong(m.group(1)), DateUtils.fromString(m.group(2)));
296 return;
297 }
298 }
299 Logging.warn(tr("Error header \"{0}\" did not match with an expected pattern", errorHeader));
300 handleUploadConflictForUnknownConflict();
301 }
302
303 /**
304 * handles an precondition failed conflict, i.e. an error indicated by a HTTP return code 412.
305 *
306 * @param e the exception
307 */
308 protected void handlePreconditionFailed(OsmApiException e) {
309 // in the worst case, ExceptionUtil.parsePreconditionFailed is executed trice - should not be too expensive
310 Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict = ExceptionUtil.parsePreconditionFailed(e.getErrorHeader());
311 if (conflict != null) {
312 handleUploadPreconditionFailedConflict(e, conflict);
313 } else {
314 Logging.warn(tr("Error header \"{0}\" did not match with an expected pattern", e.getErrorHeader()));
315 ExceptionDialogUtil.explainPreconditionFailed(e);
316 }
317 }
318
319 /**
320 * Handles an error which is caused by a delete request for an already deleted
321 * {@link OsmPrimitive} on the server, i.e. a HTTP response code of 410.
322 * Note that an <strong>update</strong> on an already deleted object results
323 * in a 409, not a 410.
324 *
325 * @param e the exception
326 */
327 protected void handleGone(OsmApiPrimitiveGoneException e) {
328 if (e.isKnownPrimitive()) {
329 UpdateSelectionAction.handlePrimitiveGoneException(e.getPrimitiveId(), e.getPrimitiveType());
330 } else {
331 ExceptionDialogUtil.explainGoneForUnknownPrimitive(e);
332 }
333 }
334
335 /**
336 * error handler for any exception thrown during upload
337 *
338 * @param e the exception
339 */
340 protected void handleFailedUpload(Exception e) {
341 // API initialization failed. Notify the user and return.
342 //
343 if (e instanceof OsmApiInitializationException) {
344 ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException) e);
345 return;
346 }
347
348 if (e instanceof OsmApiPrimitiveGoneException) {
349 handleGone((OsmApiPrimitiveGoneException) e);
350 return;
351 }
352 if (e instanceof OsmApiException) {
353 OsmApiException ex = (OsmApiException) e;
354 if (ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) {
355 // There was an upload conflict. Let the user decide whether and how to resolve it
356 handleUploadConflict(ex);
357 return;
358 } else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) {
359 // There was a precondition failed. Notify the user.
360 handlePreconditionFailed(ex);
361 return;
362 } else if (ex.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
363 // Tried to update or delete a primitive which never existed on the server?
364 ExceptionDialogUtil.explainNotFound(ex);
365 return;
366 }
367 }
368
369 ExceptionDialogUtil.explainException(e);
370 }
371}
Note: See TracBrowser for help on using the repository browser.