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

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

Improved user feedback in case of timed retries after InternalServerErrors
Improved user feedback in case of problems with closing changesets

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