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

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

Improved exception handling

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