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

Last change on this file since 2284 was 2273, checked in by jttt, 15 years ago

Replace testing for id <= 0 with isNew() method

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