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

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

new: replaced global conflict list by conflict list per layer, similar to datasets

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