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

Last change on this file since 1688 was 1688, checked in by stoecker, 15 years ago

fixed #2744 - server url defaults

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