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

Last change on this file since 12542 was 12452, checked in by bastiK, 7 years ago

see #14794 - javadoc for the josm.gui.io package

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