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

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

fixed #3393: loooong delay when using presets with a large osm file - further improvement; next step could be to turn UploadHooks into an asynchronous task; ApiPreconditionChecker loops over all keys in all upload primitives!
fixed #3435: Switching the changeset type deletes any changeset tags that have been set
fixed #3430: Entering comment=* manually in the changeset tags dialog gets ignored
fixed #3431: Upload dialog pops up again if no comment is provided
fixed #3429: created_by=* includes the wrong language when uploading from a new layer

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