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

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

fixed #2759: OsmApiException message is crippled by showMessageDialog

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