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

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

JosmAction is now a LayerChangeListener and a SelectionChangeListener
updated all JosmActions
fixed #3018: Make sure tools menu entries (and actions) are deactivated when no layer

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