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

Last change on this file since 2175 was 2175, checked in by stoecker, 15 years ago

fixed i18n problem

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