source: josm/trunk/src/org/openstreetmap/josm/actions/UploadAction.java@ 2198

Last change on this file since 2198 was 2198, checked in by Gubaer, 15 years ago

fixed #3249: Resolve conflicts between invisible and deleted primitives automatically

  • Property svn:eol-style set to native
File size: 24.1 KB
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.ActionEvent;
7import java.awt.event.KeyEvent;
8import java.io.IOException;
9import java.net.HttpURLConnection;
10import java.text.SimpleDateFormat;
11import java.util.Collection;
12import java.util.Date;
13import java.util.HashSet;
14import java.util.LinkedList;
15import java.util.logging.Logger;
16import java.util.regex.Matcher;
17import java.util.regex.Pattern;
18
19import javax.swing.JOptionPane;
20
21import org.openstreetmap.josm.Main;
22import org.openstreetmap.josm.actions.upload.ApiPreconditionCheckerHook;
23import org.openstreetmap.josm.actions.upload.RelationUploadOrderHook;
24import org.openstreetmap.josm.actions.upload.UploadHook;
25import org.openstreetmap.josm.actions.upload.UploadParameterHook;
26import org.openstreetmap.josm.data.APIDataSet;
27import org.openstreetmap.josm.data.conflict.ConflictCollection;
28import org.openstreetmap.josm.data.osm.Changeset;
29import org.openstreetmap.josm.data.osm.DataSet;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
32import org.openstreetmap.josm.gui.DefaultNameFormatter;
33import org.openstreetmap.josm.gui.ExceptionDialogUtil;
34import org.openstreetmap.josm.gui.PleaseWaitRunnable;
35import org.openstreetmap.josm.gui.io.UploadDialog;
36import org.openstreetmap.josm.gui.layer.OsmDataLayer;
37import org.openstreetmap.josm.gui.progress.ProgressMonitor;
38import org.openstreetmap.josm.io.OsmApi;
39import org.openstreetmap.josm.io.OsmApiException;
40import org.openstreetmap.josm.io.OsmApiInitializationException;
41import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
42import org.openstreetmap.josm.io.OsmChangesetCloseException;
43import org.openstreetmap.josm.io.OsmServerWriter;
44import org.openstreetmap.josm.io.OsmTransferException;
45import org.openstreetmap.josm.tools.DateUtils;
46import org.openstreetmap.josm.tools.Shortcut;
47import org.xml.sax.SAXException;
48
49
50/**
51 * Action that opens a connection to the osm server and uploads all changes.
52 *
53 * An dialog is displayed asking the user to specify a rectangle to grab.
54 * The url and account settings from the preferences are used.
55 *
56 * If the upload fails this action offers various options to resolve conflicts.
57 *
58 * @author imi
59 */
60public class UploadAction extends JosmAction{
61 static private Logger logger = Logger.getLogger(UploadAction.class.getName());
62 /**
63 * The list of upload hooks. These hooks will be called one after the other
64 * when the user wants to upload data. Plugins can insert their own hooks here
65 * if they want to be able to veto an upload.
66 *
67 * Be default, the standard upload dialog is the only element in the list.
68 * Plugins should normally insert their code before that, so that the upload
69 * dialog is the last thing shown before upload really starts; on occasion
70 * however, a plugin might also want to insert something after that.
71 */
72 private static final LinkedList<UploadHook> uploadHooks = new LinkedList<UploadHook>();
73 static {
74 /**
75 * Checks server capabilities before upload.
76 */
77 uploadHooks.add(new ApiPreconditionCheckerHook());
78
79 /**
80 * Adjusts the upload order of new relations
81 */
82 uploadHooks.add(new RelationUploadOrderHook());
83
84 /**
85 * Displays a screen where the actions that would be taken are displayed and
86 * give the user the possibility to cancel the upload.
87 */
88 uploadHooks.add(new UploadParameterHook());
89 }
90
91 /**
92 * Registers an upload hook. Adds the hook at the first position of the upload hooks.
93 *
94 * @param hook the upload hook. Ignored if null.
95 */
96 public static void registerUploadHook(UploadHook hook) {
97 if(hook == null) return;
98 if (!uploadHooks.contains(hook)) {
99 uploadHooks.add(0,hook);
100 }
101 }
102
103 /**
104 * Unregisters an upload hook. Removes the hook from the list of upload hooks.
105 *
106 * @param hook the upload hook. Ignored if null.
107 */
108 public static void unregisterUploadHook(UploadHook hook) {
109 if(hook == null) return;
110 if (uploadHooks.contains(hook)) {
111 uploadHooks.remove(hook);
112 }
113 }
114
115 public UploadAction() {
116 super(tr("Upload to OSM..."), "upload", tr("Upload all changes to the OSM server."),
117 Shortcut.registerShortcut("file:upload", tr("File: {0}", tr("Upload to OSM...")), KeyEvent.VK_U, Shortcut.GROUPS_ALT1+Shortcut.GROUP_HOTKEY), true);
118 }
119
120 /**
121 * Refreshes the enabled state
122 *
123 */
124 @Override
125 protected void updateEnabledState() {
126 setEnabled(getEditLayer() != null);
127 }
128
129 public boolean checkPreUploadConditions(OsmDataLayer layer) {
130 return checkPreUploadConditions(layer, new APIDataSet(layer.data));
131 }
132
133 public boolean checkPreUploadConditions(OsmDataLayer layer, APIDataSet apiData) {
134 ConflictCollection conflicts = layer.getConflicts();
135 if (conflicts !=null && !conflicts.isEmpty()) {
136 JOptionPane.showMessageDialog(
137 Main.parent,
138 tr("<html>There are unresolved conflicts in layer ''{0}''.<br>"
139 + "You have to resolve them first.</html>", layer.getName()),
140 tr("Warning"),
141 JOptionPane.WARNING_MESSAGE
142 );
143 return false;
144 }
145 // Call all upload hooks in sequence. The upload confirmation dialog
146 // is one of these.
147 for(UploadHook hook : uploadHooks)
148 if(!hook.checkUpload(apiData))
149 return false;
150
151 return true;
152 }
153
154 public void actionPerformed(ActionEvent e) {
155 if (!isEnabled())
156 return;
157 if (Main.map == null) {
158 JOptionPane.showMessageDialog(
159 Main.parent,
160 tr("Nothing to upload. Get some data first."),
161 tr("Warning"),
162 JOptionPane.WARNING_MESSAGE
163 );
164 return;
165 }
166
167 APIDataSet apiData = new APIDataSet(Main.main.getCurrentDataSet());
168 if (apiData.isEmpty()) {
169 JOptionPane.showMessageDialog(
170 Main.parent,
171 tr("No changes to upload."),
172 tr("Warning"),
173 JOptionPane.INFORMATION_MESSAGE
174 );
175 return;
176 }
177 if (!checkPreUploadConditions(Main.map.mapView.getEditLayer(), apiData))
178 return;
179 Main.worker.execute(
180 createUploadTask(
181 Main.map.mapView.getEditLayer(),
182 apiData.getPrimitives(),
183 UploadDialog.getUploadDialog().getChangeset(),
184 UploadDialog.getUploadDialog().isDoCloseAfterUpload()
185 )
186 );
187 }
188
189 /**
190 * Synchronizes the local state of an {@see OsmPrimitive} with its state on the
191 * server. The method uses an individual GET for the primitive.
192 *
193 * @param id the primitive ID
194 */
195 protected void synchronizePrimitive(final OsmPrimitiveType type, final long id) {
196 Main.worker.execute(new UpdatePrimitiveTask(type, id));
197 }
198
199 /**
200 * Synchronizes the local state of the dataset with the state on the server.
201 *
202 * Reuses the functionality of {@see UpdateDataAction}.
203 *
204 * @see UpdateDataAction#actionPerformed(ActionEvent)
205 */
206 protected void synchronizeDataSet() {
207 UpdateDataAction act = new UpdateDataAction();
208 act.actionPerformed(new ActionEvent(this,0,""));
209 }
210
211 /**
212 * Handles the case that a conflict in a specific {@see OsmPrimitive} was detected while
213 * uploading
214 *
215 * @param primitiveType the type of the primitive, either <code>node</code>, <code>way</code> or
216 * <code>relation</code>
217 * @param id the id of the primitive
218 * @param serverVersion the version of the primitive on the server
219 * @param myVersion the version of the primitive in the local dataset
220 */
221 protected void handleUploadConflictForKnownConflict(OsmPrimitiveType primitiveType, long id, String serverVersion, String myVersion) {
222 Object[] options = new Object[] {
223 tr("Synchronize {0} {1} only", tr(primitiveType.getAPIName()), id),
224 tr("Synchronize entire dataset"),
225 tr("Cancel")
226 };
227 Object defaultOption = options[0];
228 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
229 + "of your nodes, ways, or relations.<br>"
230 + "The conflict is caused by the <strong>{0}</strong> with id <strong>{1}</strong>,<br>"
231 + "the server has version {2}, your version is {3}.<br>"
232 + "<br>"
233 + "Click <strong>{4}</strong> to synchronize the conflicting primitive only.<br>"
234 + "Click <strong>{5}</strong> to synchronize the entire local dataset with the server.<br>"
235 + "Click <strong>{6}</strong> to abort and continue editing.<br></html>",
236 tr(primitiveType.getAPIName()), id, serverVersion, myVersion,
237 options[0], options[1], options[2]
238 );
239 int optionsType = JOptionPane.YES_NO_CANCEL_OPTION;
240 int ret = JOptionPane.showOptionDialog(
241 null,
242 msg,
243 tr("Conflict detected"),
244 optionsType,
245 JOptionPane.ERROR_MESSAGE,
246 null,
247 options,
248 defaultOption
249 );
250 switch(ret) {
251 case JOptionPane.CLOSED_OPTION: return;
252 case JOptionPane.CANCEL_OPTION: return;
253 case 0: synchronizePrimitive(primitiveType, id); break;
254 case 1: synchronizeDataSet(); break;
255 default:
256 // should not happen
257 throw new IllegalStateException(tr("Unexpected return value. Got {0}.", ret));
258 }
259 }
260
261 /**
262 * Handles the case that a conflict was detected while uploading where we don't
263 * know what {@see OsmPrimitive} actually caused the conflict (for whatever reason)
264 *
265 */
266 protected void handleUploadConflictForUnknownConflict() {
267 Object[] options = new Object[] {
268 tr("Synchronize entire dataset"),
269 tr("Cancel")
270 };
271 Object defaultOption = options[0];
272 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
273 + "of your nodes, ways, or relations.<br>"
274 + "<br>"
275 + "Click <strong>{0}</strong> to synchronize the entire local dataset with the server.<br>"
276 + "Click <strong>{1}</strong> to abort and continue editing.<br></html>",
277 options[0], options[1]
278 );
279 int optionsType = JOptionPane.YES_NO_OPTION;
280 int ret = JOptionPane.showOptionDialog(
281 null,
282 msg,
283 tr("Conflict detected"),
284 optionsType,
285 JOptionPane.ERROR_MESSAGE,
286 null,
287 options,
288 defaultOption
289 );
290 switch(ret) {
291 case JOptionPane.CLOSED_OPTION: return;
292 case 1: return;
293 case 0: synchronizeDataSet(); break;
294 default:
295 // should not happen
296 throw new IllegalStateException(tr("Unexpected return value. Got {0}.", ret));
297 }
298 }
299
300 /**
301 * Handles the case that a conflict was detected while uploading where we don't
302 * know what {@see OsmPrimitive} actually caused the conflict (for whatever reason)
303 *
304 */
305 protected void handleUploadConflictForClosedChangeset(long changsetId, Date d) {
306 String msg = tr("<html>Uploading <strong>failed</strong> because you''ve been using<br>"
307 + "changeset {0} which was already closed at {1}.<br>"
308 + "Please upload again with a new or an existing open changeset.</html>",
309 changsetId, new SimpleDateFormat().format(d)
310 );
311 JOptionPane.showMessageDialog(
312 Main.parent,
313 msg,
314 tr("Changeset closed"),
315 JOptionPane.ERROR_MESSAGE
316 );
317 }
318
319 /**
320 * handles an upload conflict, i.e. an error indicated by a HTTP return code 409.
321 *
322 * @param e the exception
323 */
324 protected void handleUploadConflict(OsmApiException e) {
325 String pattern = "Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)";
326 Pattern p = Pattern.compile(pattern);
327 Matcher m = p.matcher(e.getErrorHeader());
328 if (m.matches()) {
329 handleUploadConflictForKnownConflict(OsmPrimitiveType.from(m.group(3)), Long.parseLong(m.group(4)), m.group(2),m.group(1));
330 return;
331 }
332 pattern ="The changeset (\\d+) was closed at (.*)";
333 p = Pattern.compile(pattern);
334 m = p.matcher(e.getErrorHeader());
335 if (m.matches()) {
336 handleUploadConflictForClosedChangeset(Long.parseLong(m.group(1)), DateUtils.fromString(m.group(2)));
337 return;
338 }
339 logger.warning(tr("Warning: error header \"{0}\" did not match expected pattern \"{1}\"", e.getErrorHeader(),pattern));
340 handleUploadConflictForUnknownConflict();
341 }
342
343 /**
344 * Handles an error due to a delete request on an already deleted
345 * {@see OsmPrimitive}, i.e. a HTTP response code 410, where we know what
346 * {@see OsmPrimitive} is responsible for the error.
347 *
348 * Reuses functionality of the {@see UpdateSelectionAction} to resolve
349 * conflicts due to mismatches in the deleted state.
350 *
351 * @param primitiveType the type of the primitive
352 * @param id the id of the primitive
353 *
354 * @see UpdateSelectionAction#handlePrimitiveGoneException(long)
355 */
356 protected void handleGoneForKnownPrimitive(OsmPrimitiveType primitiveType, long id) {
357 UpdateSelectionAction act = new UpdateSelectionAction();
358 act.handlePrimitiveGoneException(id,primitiveType);
359 }
360
361 /**
362 * Handles an error which is caused by a delete request for an already deleted
363 * {@see OsmPrimitive} on the server, i.e. a HTTP response code of 410.
364 * Note that an <strong>update</strong> on an already deleted object results
365 * in a 409, not a 410.
366 *
367 * @param e the exception
368 */
369 protected void handleGone(OsmApiPrimitiveGoneException e) {
370 if (e.isKnownPrimitive()) {
371 handleGoneForKnownPrimitive(e.getPrimitiveType(), e.getPrimitiveId());
372 } else {
373 ExceptionDialogUtil.explainGoneForUnknownPrimitive(e);
374 }
375 }
376
377
378 /**
379 * error handler for any exception thrown during upload
380 *
381 * @param e the exception
382 */
383 protected void handleFailedUpload(Exception e) {
384 // API initialization failed. Notify the user and return.
385 //
386 if (e instanceof OsmApiInitializationException) {
387 ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException)e);
388 return;
389 }
390
391 if (e instanceof OsmChangesetCloseException) {
392 ExceptionDialogUtil.explainOsmChangesetCloseException((OsmChangesetCloseException)e);
393 return;
394 }
395 if (e instanceof OsmApiPrimitiveGoneException) {
396 handleGone((OsmApiPrimitiveGoneException)e);
397 return;
398 }
399 if (e instanceof OsmApiException) {
400 OsmApiException ex = (OsmApiException)e;
401 // There was an upload conflict. Let the user decide whether
402 // and how to resolve it
403 //
404 if(ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) {
405 handleUploadConflict(ex);
406 return;
407 }
408 // There was a precondition failed. Notify the user.
409 //
410 else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) {
411 ExceptionDialogUtil.explainPreconditionFailed(ex);
412 return;
413 }
414 // Tried to update or delete a primitive which never existed on
415 // the server?
416 //
417 else if (ex.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
418 ExceptionDialogUtil.explainNotFound(ex);
419 return;
420 }
421 // any other API exception
422 //
423 else {
424 ex.printStackTrace();
425 String msg = tr("<html>Uploading <strong>failed</strong>."
426 + "<br>"
427 + "{0}"
428 + "</html>",
429 ex.getDisplayMessage()
430 );
431 JOptionPane.showMessageDialog(
432 Main.map,
433 msg,
434 tr("Upload to OSM API failed"),
435 JOptionPane.ERROR_MESSAGE
436 );
437 return;
438 }
439 }
440
441 ExceptionDialogUtil.explainException(e);
442 }
443
444 /**
445 * The asynchronous task to update a specific id
446 *
447 */
448 class UpdatePrimitiveTask extends PleaseWaitRunnable {
449
450 private boolean uploadCancelled = false;
451 private boolean uploadFailed = false;
452 private Exception lastException = null;
453 private long id;
454 private OsmPrimitiveType type;
455
456 public UpdatePrimitiveTask(OsmPrimitiveType type, long id) {
457 super(tr("Updating primitive"),false /* don't ignore exceptions */);
458 this.id = id;
459 this.type = type;
460 }
461
462 @Override protected void realRun() throws SAXException, IOException {
463 try {
464 UpdateSelectionAction act = new UpdateSelectionAction();
465 act.updatePrimitive(type, id);
466 } catch (Exception sxe) {
467 if (uploadCancelled) {
468 System.out.println("Ignoring exception caught because upload is cancelled. Exception is: " + sxe.toString());
469 return;
470 }
471 uploadFailed = true;
472 lastException = sxe;
473 }
474 }
475
476 @Override protected void finish() {
477 if (uploadFailed) {
478 handleFailedUpload(lastException);
479 }
480 }
481
482 @Override protected void cancel() {
483 OsmApi.getOsmApi().cancel();
484 uploadCancelled = true;
485 }
486 }
487
488 public UploadPrimitivesTask createUploadTask(OsmDataLayer layer, Collection<OsmPrimitive> toUpload, Changeset changeset, boolean closeChangesetAfterUpload) {
489 return new UploadPrimitivesTask(layer, toUpload, changeset, closeChangesetAfterUpload);
490 }
491
492 /**
493 * The task for uploading a collection of primitives
494 *
495 */
496 public class UploadPrimitivesTask extends PleaseWaitRunnable {
497 private boolean uploadCancelled = false;
498 private Exception lastException = null;
499 private Collection <OsmPrimitive> toUpload;
500 private OsmServerWriter writer;
501 private OsmDataLayer layer;
502 private Changeset changeset;
503 private boolean closeChangesetAfterUpload;
504 private HashSet<OsmPrimitive> processedPrimitives;
505
506 /**
507 *
508 * @param layer the OSM data layer for which data is uploaded
509 * @param toUpload the collection of primitives to upload
510 * @param changeset the changeset to use for uploading
511 * @param closeChangesetAfterUpload true, if the changeset is to be closed after uploading
512 */
513 private UploadPrimitivesTask(OsmDataLayer layer, Collection <OsmPrimitive> toUpload, Changeset changeset, boolean closeChangesetAfterUpload) {
514 super(tr("Uploading data for layer ''{0}''", layer.getName()),false /* don't ignore exceptions */);
515 this.toUpload = toUpload;
516 this.layer = layer;
517 this.changeset = changeset;
518 this.closeChangesetAfterUpload = closeChangesetAfterUpload;
519 this.processedPrimitives = new HashSet<OsmPrimitive>();
520 }
521
522 protected OsmPrimitive getPrimitive(OsmPrimitiveType type, long id) {
523 for (OsmPrimitive p: toUpload) {
524 if (OsmPrimitiveType.from(p).equals(type) && p.getId() == id)
525 return p;
526 }
527 return null;
528 }
529
530 /**
531 * Retries to recover the upload operation from an exception which was thrown because
532 * an uploaded primitive was already deleted on the server.
533 *
534 * @param e the exception throw by the API
535 * @param monitor a progress monitor
536 * @throws OsmTransferException thrown if we can't recover from the exception
537 */
538 protected void recoverFromGoneOnServer(OsmApiPrimitiveGoneException e, ProgressMonitor monitor) throws OsmTransferException{
539 if (!e.isKnownPrimitive()) throw e;
540 OsmPrimitive p = getPrimitive(e.getPrimitiveType(), e.getPrimitiveId());
541 if (p == null) throw e;
542 if (p.isDeleted()) {
543 // we tried to delete an already deleted primitive.
544 //
545 System.out.println(tr("Warning: primitive ''{0}'' is already deleted on the server. Skipping this primitive and retrying to upload.", p.getDisplayName(DefaultNameFormatter.getInstance())));
546 processedPrimitives.addAll(writer.getProcessedPrimitives());
547 processedPrimitives.add(p);
548 toUpload.removeAll(processedPrimitives);
549 return;
550 }
551 // exception was thrown because we tried to *update* an already deleted
552 // primitive. We can't resolve this automatically. Re-throw exception,
553 // a conflict is going to be created later.
554 throw e;
555 }
556
557 @Override protected void realRun() throws SAXException, IOException {
558 writer = new OsmServerWriter();
559 try {
560 //
561 while(true) {
562 try {
563 ProgressMonitor monitor = progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false);
564 writer.uploadOsm(layer.data.version, toUpload, changeset, monitor);
565 processedPrimitives.addAll(writer.getProcessedPrimitives());
566 // if we get here we've successfully uploaded the data. We
567 // can exit the loop.
568 //
569 break;
570 } catch(OsmApiPrimitiveGoneException e) {
571 // try to recover from the 410 Gone
572 recoverFromGoneOnServer(e, getProgressMonitor());
573 }
574 }
575 // if required close the changeset
576 //
577 if (closeChangesetAfterUpload) {
578 if (changeset != null && changeset.getId() > 0) {
579 OsmApi.getOsmApi().closeChangeset(changeset, progressMonitor.createSubTaskMonitor(0,false));
580 }
581 }
582 } catch (Exception e) {
583 if (uploadCancelled) {
584 System.out.println("Ignoring exception caught because upload is cancelled. Exception is: " + e.toString());
585 return;
586 }
587 lastException = e;
588 }
589 }
590
591 @Override protected void finish() {
592 if (uploadCancelled)
593 return;
594
595 // we always clean up the data, even in case of errors. It's possible the data was
596 // partially uploaded
597 //
598 layer.cleanupAfterUpload(processedPrimitives);
599 DataSet.fireSelectionChanged(layer.data.getSelected());
600 layer.fireDataChange();
601 if (lastException != null) {
602 handleFailedUpload(lastException);
603 }
604 layer.onPostUploadToServer();
605 UploadDialog.getUploadDialog().setOrUpdateChangeset(changeset);
606 }
607
608 @Override protected void cancel() {
609 uploadCancelled = true;
610 if (writer != null) {
611 writer.cancel();
612 }
613 }
614 }
615}
Note: See TracBrowser for help on using the repository browser.