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

Last change on this file since 12806 was 12636, checked in by Don-vip, 7 years ago

see #15182 - deprecate Main.getLayerManager(). Replacement: gui.MainApplication.getLayerManager()

  • Property svn:eol-style set to native
File size: 15.6 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.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 = new ButtonSpec[] {
132 new ButtonSpec(
133 lbl,
134 ImageProvider.get("updatedata"),
135 null,
136 null
137 ),
138 new ButtonSpec(
139 tr("Synchronize entire dataset"),
140 ImageProvider.get("updatedata"),
141 null,
142 null
143 ),
144 new ButtonSpec(
145 tr("Cancel"),
146 ImageProvider.get("cancel"),
147 null,
148 null
149 )
150 };
151 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
152 + "of your nodes, ways, or relations.<br>"
153 + "The conflict is caused by the <strong>{0}</strong> with id <strong>{1}</strong>,<br>"
154 + "the server has version {2}, your version is {3}.<br>"
155 + "<br>"
156 + "Click <strong>{4}</strong> to synchronize the conflicting primitive only.<br>"
157 + "Click <strong>{5}</strong> to synchronize the entire local dataset with the server.<br>"
158 + "Click <strong>{6}</strong> to abort and continue editing.<br></html>",
159 tr(primitiveType.getAPIName()), id, serverVersion, myVersion,
160 spec[0].text, spec[1].text, spec[2].text
161 );
162 int ret = HelpAwareOptionPane.showOptionDialog(
163 Main.parent,
164 msg,
165 tr("Conflicts detected"),
166 JOptionPane.ERROR_MESSAGE,
167 null,
168 spec,
169 spec[0],
170 "/Concepts/Conflict"
171 );
172 switch(ret) {
173 case 0: synchronizePrimitive(primitiveType, id); break;
174 case 1: synchronizeDataSet(); break;
175 default: return;
176 }
177 }
178
179 /**
180 * Handles the case that a conflict was detected while uploading where we don't
181 * know what {@link OsmPrimitive} actually caused the conflict (for whatever reason)
182 *
183 */
184 protected void handleUploadConflictForUnknownConflict() {
185 ButtonSpec[] spec = new ButtonSpec[] {
186 new ButtonSpec(
187 tr("Synchronize entire dataset"),
188 ImageProvider.get("updatedata"),
189 null,
190 null
191 ),
192 new ButtonSpec(
193 tr("Cancel"),
194 ImageProvider.get("cancel"),
195 null,
196 null
197 )
198 };
199 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
200 + "of your nodes, ways, or relations.<br>"
201 + "<br>"
202 + "Click <strong>{0}</strong> to synchronize the entire local dataset with the server.<br>"
203 + "Click <strong>{1}</strong> to abort and continue editing.<br></html>",
204 spec[0].text, spec[1].text
205 );
206 int ret = HelpAwareOptionPane.showOptionDialog(
207 Main.parent,
208 msg,
209 tr("Conflicts detected"),
210 JOptionPane.ERROR_MESSAGE,
211 null,
212 spec,
213 spec[0],
214 ht("/Concepts/Conflict")
215 );
216 if (ret == 0) {
217 synchronizeDataSet();
218 }
219 }
220
221 /**
222 * Handles the case that a conflict was detected while uploading where we don't
223 * know what {@link OsmPrimitive} actually caused the conflict (for whatever reason)
224 * @param changesetId changeset ID
225 * @param d changeset date
226 */
227 protected void handleUploadConflictForClosedChangeset(long changesetId, Date d) {
228 String msg = tr("<html>Uploading <strong>failed</strong> because you have been using<br>"
229 + "changeset {0} which was already closed at {1}.<br>"
230 + "Please upload again with a new or an existing open changeset.</html>",
231 changesetId, DateUtils.formatDateTime(d, DateFormat.SHORT, DateFormat.SHORT)
232 );
233 JOptionPane.showMessageDialog(
234 Main.parent,
235 msg,
236 tr("Changeset closed"),
237 JOptionPane.ERROR_MESSAGE
238 );
239 }
240
241 /**
242 * Handles the case where deleting a node failed because it is still in use in
243 * a non-deleted way on the server.
244 * @param e exception
245 * @param conflict conflict
246 */
247 protected void handleUploadPreconditionFailedConflict(OsmApiException e, Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict) {
248 ButtonSpec[] options = new ButtonSpec[] {
249 new ButtonSpec(
250 tr("Prepare conflict resolution"),
251 ImageProvider.get("ok"),
252 tr("Click to download all referring objects for {0}", conflict.a),
253 null /* no specific help context */
254 ),
255 new ButtonSpec(
256 tr("Cancel"),
257 ImageProvider.get("cancel"),
258 tr("Click to cancel and to resume editing the map"),
259 null /* no specific help context */
260 )
261 };
262 String msg = ExceptionUtil.explainPreconditionFailed(e).replace("</html>", "<br><br>" + tr(
263 "Click <strong>{0}</strong> to load them now.<br>"
264 + "If necessary JOSM will create conflicts which you can resolve in the Conflict Resolution Dialog.",
265 options[0].text)) + "</html>";
266 int ret = HelpAwareOptionPane.showOptionDialog(
267 Main.parent,
268 msg,
269 tr("Object still in use"),
270 JOptionPane.ERROR_MESSAGE,
271 null,
272 options,
273 options[0],
274 "/Action/Upload#NodeStillInUseInWay"
275 );
276 if (ret == 0) {
277 DownloadReferrersAction.downloadReferrers(MainApplication.getLayerManager().getEditLayer(), Arrays.asList(conflict.a));
278 }
279 }
280
281 /**
282 * handles an upload conflict, i.e. an error indicated by a HTTP return code 409.
283 *
284 * @param e the exception
285 */
286 protected void handleUploadConflict(OsmApiException e) {
287 final String errorHeader = e.getErrorHeader();
288 if (errorHeader != null) {
289 Pattern p = Pattern.compile("Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)");
290 Matcher m = p.matcher(errorHeader);
291 if (m.matches()) {
292 handleUploadConflictForKnownConflict(OsmPrimitiveType.from(m.group(3)), Long.parseLong(m.group(4)), m.group(2), m.group(1));
293 return;
294 }
295 p = Pattern.compile("The changeset (\\d+) was closed at (.*)");
296 m = p.matcher(errorHeader);
297 if (m.matches()) {
298 handleUploadConflictForClosedChangeset(Long.parseLong(m.group(1)), DateUtils.fromString(m.group(2)));
299 return;
300 }
301 }
302 Logging.warn(tr("Error header \"{0}\" did not match with an expected pattern", errorHeader));
303 handleUploadConflictForUnknownConflict();
304 }
305
306 /**
307 * handles an precondition failed conflict, i.e. an error indicated by a HTTP return code 412.
308 *
309 * @param e the exception
310 */
311 protected void handlePreconditionFailed(OsmApiException e) {
312 // in the worst case, ExceptionUtil.parsePreconditionFailed is executed trice - should not be too expensive
313 Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict = ExceptionUtil.parsePreconditionFailed(e.getErrorHeader());
314 if (conflict != null) {
315 handleUploadPreconditionFailedConflict(e, conflict);
316 } else {
317 Logging.warn(tr("Error header \"{0}\" did not match with an expected pattern", e.getErrorHeader()));
318 ExceptionDialogUtil.explainPreconditionFailed(e);
319 }
320 }
321
322 /**
323 * Handles an error which is caused by a delete request for an already deleted
324 * {@link OsmPrimitive} on the server, i.e. a HTTP response code of 410.
325 * Note that an <strong>update</strong> on an already deleted object results
326 * in a 409, not a 410.
327 *
328 * @param e the exception
329 */
330 protected void handleGone(OsmApiPrimitiveGoneException e) {
331 if (e.isKnownPrimitive()) {
332 UpdateSelectionAction.handlePrimitiveGoneException(e.getPrimitiveId(), e.getPrimitiveType());
333 } else {
334 ExceptionDialogUtil.explainGoneForUnknownPrimitive(e);
335 }
336 }
337
338 /**
339 * error handler for any exception thrown during upload
340 *
341 * @param e the exception
342 */
343 protected void handleFailedUpload(Exception e) {
344 // API initialization failed. Notify the user and return.
345 //
346 if (e instanceof OsmApiInitializationException) {
347 ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException) e);
348 return;
349 }
350
351 if (e instanceof OsmApiPrimitiveGoneException) {
352 handleGone((OsmApiPrimitiveGoneException) e);
353 return;
354 }
355 if (e instanceof OsmApiException) {
356 OsmApiException ex = (OsmApiException) e;
357 if (ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) {
358 // There was an upload conflict. Let the user decide whether and how to resolve it
359 handleUploadConflict(ex);
360 return;
361 } else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) {
362 // There was a precondition failed. Notify the user.
363 handlePreconditionFailed(ex);
364 return;
365 } else if (ex.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
366 // Tried to update or delete a primitive which never existed on the server?
367 ExceptionDialogUtil.explainNotFound(ex);
368 return;
369 }
370 }
371
372 ExceptionDialogUtil.explainException(e);
373 }
374}
Note: See TracBrowser for help on using the repository browser.