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

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

fixed #2367: panel will be empty when undocked from minimized state
cleanup in Toggle Dialog
new: toggle dialog remember position when detached
new: title in toggle dialog
NOTE: this changeset probably breaks plugins, in particular the validator plugin

  • Property svn:eol-style set to native
File size: 21.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.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.DataSet;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.gui.ExceptionDialogUtil;
32import org.openstreetmap.josm.gui.ExtendedDialog;
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 JOptionPane.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 JOptionPane.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.showDialog();
132 return;
133 }
134
135 final LinkedList<OsmPrimitive> add = new LinkedList<OsmPrimitive>();
136 final LinkedList<OsmPrimitive> update = new LinkedList<OsmPrimitive>();
137 final LinkedList<OsmPrimitive> delete = new LinkedList<OsmPrimitive>();
138 for (OsmPrimitive osm : getCurrentDataSet().allPrimitives()) {
139 if (osm.get("josm/ignore") != null) {
140 continue;
141 }
142 if (osm.id == 0 && !osm.deleted) {
143 add.addLast(osm);
144 } else if (osm.modified && !osm.deleted) {
145 update.addLast(osm);
146 } else if (osm.deleted && osm.id != 0) {
147 delete.addFirst(osm);
148 }
149 }
150
151 if (add.isEmpty() && update.isEmpty() && delete.isEmpty()) {
152 JOptionPane.showMessageDialog(
153 Main.parent,
154 tr("No changes to upload."),
155 tr("Warning"),
156 JOptionPane.WARNING_MESSAGE
157 );
158 return;
159 }
160
161 // Call all upload hooks in sequence. The upload confirmation dialog
162 // is one of these.
163 for(UploadHook hook : uploadHooks)
164 if(!hook.checkUpload(add, update, delete))
165 return;
166
167 final Collection<OsmPrimitive> all = new LinkedList<OsmPrimitive>();
168 all.addAll(add);
169 all.addAll(update);
170 all.addAll(delete);
171
172 Main.worker.execute(new UploadDiffTask(all));
173 }
174
175 /**
176 * Synchronizes the local state of an {@see OsmPrimitive} with its state on the
177 * server. The method uses an individual GET for the primitive.
178 *
179 * @param id the primitive ID
180 */
181 protected void synchronizePrimitive(final String id) {
182 Main.worker.execute(new UpdatePrimitiveTask(Long.parseLong(id)));
183 }
184
185 /**
186 * Synchronizes the local state of the dataset with the state on the server.
187 *
188 * Reuses the functionality of {@see UpdateDataAction}.
189 *
190 * @see UpdateDataAction#actionPerformed(ActionEvent)
191 */
192 protected void synchronizeDataSet() {
193 UpdateDataAction act = new UpdateDataAction();
194 act.actionPerformed(new ActionEvent(this,0,""));
195 }
196
197 /**
198 * Handles the case that a conflict in a specific {@see OsmPrimitive} was detected while
199 * uploading
200 *
201 * @param primitiveType the type of the primitive, either <code>node</code>, <code>way</code> or
202 * <code>relation</code>
203 * @param id the id of the primitive
204 * @param serverVersion the version of the primitive on the server
205 * @param myVersion the version of the primitive in the local dataset
206 */
207 protected void handleUploadConflictForKnownConflict(String primitiveType, String id, String serverVersion, String myVersion) {
208 Object[] options = new Object[] {
209 tr("Synchronize {0} {1} only", tr(primitiveType), id),
210 tr("Synchronize entire dataset"),
211 tr("Cancel")
212 };
213 Object defaultOption = options[0];
214 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
215 + "of your nodes, ways, or relations.<br>"
216 + "The conflict is caused by the <strong>{0}</strong> with id <strong>{1}</strong>,<br>"
217 + "the server has version {2}, your version is {3}.<br>"
218 + "<br>"
219 + "Click <strong>{4}</strong> to synchronize the conflicting primitive only.<br>"
220 + "Click <strong>{5}</strong> to synchronize the entire local dataset with the server.<br>"
221 + "Click <strong>{6}</strong> to abort and continue editing.<br></html>",
222 tr(primitiveType), id, serverVersion, myVersion,
223 options[0], options[1], options[2]
224 );
225 int optionsType = JOptionPane.YES_NO_CANCEL_OPTION;
226 int ret = JOptionPane.showOptionDialog(
227 null,
228 msg,
229 tr("Conflict detected"),
230 optionsType,
231 JOptionPane.ERROR_MESSAGE,
232 null,
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 = JOptionPane.showOptionDialog(
267 null,
268 msg,
269 tr("Conflict detected"),
270 optionsType,
271 JOptionPane.ERROR_MESSAGE,
272 null,
273 options,
274 defaultOption
275 );
276 switch(ret) {
277 case JOptionPane.CLOSED_OPTION: return;
278 case 1: return;
279 case 0: synchronizeDataSet(); break;
280 default:
281 // should not happen
282 throw new IllegalStateException(tr("unexpected return value. Got {0}", ret));
283 }
284 }
285
286 /**
287 * handles an upload conflict, i.e. an error indicated by a HTTP return code 409.
288 *
289 * @param e the exception
290 */
291 protected void handleUploadConflict(OsmApiException e) {
292 String pattern = "Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)";
293 Pattern p = Pattern.compile(pattern);
294 Matcher m = p.matcher(e.getErrorHeader());
295 if (m.matches()) {
296 handleUploadConflictForKnownConflict(m.group(3), m.group(4), m.group(2),m.group(1));
297 } else {
298 logger.warning(tr("Warning: error header \"{0}\" did not match expected pattern \"{1}\"", e.getErrorHeader(),pattern));
299 handleUploadConflictForUnknownConflict();
300 }
301 }
302
303 /**
304 * Handles an error due to a delete request on an already deleted
305 * {@see OsmPrimitive}, i.e. a HTTP response code 410, where we know what
306 * {@see OsmPrimitive} is responsible for the error.
307 *
308 * Reuses functionality of the {@see UpdateSelectionAction} to resolve
309 * conflicts due to mismatches in the deleted state.
310 *
311 * @param primitiveType the type of the primitive
312 * @param id the id of the primitive
313 *
314 * @see UpdateSelectionAction#handlePrimitiveGoneException(long)
315 */
316 protected void handleGoneForKnownPrimitive(String primitiveType, String id) {
317 UpdateSelectionAction act = new UpdateSelectionAction();
318 act.handlePrimitiveGoneException(Long.parseLong(id));
319 }
320
321 /**
322 * Handles an error which is caused by a delete request for an already deleted
323 * {@see OsmPrimitive} on the server, i.e. a HTTP response code of 410.
324 * Note that an <strong>update</strong> on an already deleted object results
325 * in a 409, not a 410.
326 *
327 * @param e the exception
328 */
329 protected void handleGone(OsmApiException e) {
330 String pattern = "The (\\S+) with the id (\\d+) has already been deleted";
331 Pattern p = Pattern.compile(pattern);
332 Matcher m = p.matcher(e.getErrorHeader());
333 if (m.matches()) {
334 handleGoneForKnownPrimitive(m.group(1), m.group(2));
335 } else {
336 logger.warning(tr("Error header \"{0}\" does not match expected pattern \"{1}\"",e.getErrorHeader(), pattern));
337 ExceptionDialogUtil.explainGoneForUnknownPrimitive(e);
338 }
339 }
340
341
342 /**
343 * error handler for any exception thrown during upload
344 *
345 * @param e the exception
346 */
347 protected void handleFailedUpload(Exception e) {
348 // API initialization failed. Notify the user and return.
349 //
350 if (e instanceof OsmApiInitializationException) {
351 ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException)e);
352 return;
353 }
354
355 if (e instanceof OsmApiException) {
356 OsmApiException ex = (OsmApiException)e;
357 // There was an upload conflict. Let the user decide whether
358 // and how to resolve it
359 //
360 if(ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) {
361 handleUploadConflict(ex);
362 return;
363 }
364 // There was a precondition failed. Notify the user.
365 //
366 else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) {
367 ExceptionDialogUtil.explainPreconditionFailed(ex);
368 return;
369 }
370 // Tried to delete an already deleted primitive? Let the user
371 // decide whether and how to resolve this conflict.
372 //
373 else if (ex.getResponseCode() == HttpURLConnection.HTTP_GONE) {
374 handleGone(ex);
375 return;
376 }
377 // any other API exception
378 //
379 else {
380 ex.printStackTrace();
381 String msg = tr("<html>Uploading <strong>failed</strong>."
382 + "<br>"
383 + "{0}"
384 + "</html>",
385 ex.getDisplayMessage()
386 );
387 JOptionPane.showMessageDialog(
388 Main.map,
389 msg,
390 tr("Upload to OSM API failed"),
391 JOptionPane.ERROR_MESSAGE
392 );
393 return;
394 }
395 }
396
397 ExceptionDialogUtil.explainException(e);
398 }
399
400 /**
401 * The asynchronous task to update a specific id
402 *
403 */
404 class UpdatePrimitiveTask extends PleaseWaitRunnable {
405
406 private boolean uploadCancelled = false;
407 private boolean uploadFailed = false;
408 private Exception lastException = null;
409 private long id;
410
411 public UpdatePrimitiveTask(long id) {
412 super(tr("Updating primitive"),false /* don't ignore exceptions */);
413 this.id = id;
414 }
415
416 @Override protected void realRun() throws SAXException, IOException {
417 try {
418 UpdateSelectionAction act = new UpdateSelectionAction();
419 act.updatePrimitive(id);
420 } catch (Exception sxe) {
421 if (uploadCancelled) {
422 System.out.println("Ignoring exception caught because upload is cancelled. Exception is: " + sxe.toString());
423 return;
424 }
425 uploadFailed = true;
426 lastException = sxe;
427 }
428 }
429
430 @Override protected void finish() {
431 if (uploadFailed) {
432 handleFailedUpload(lastException);
433 }
434 }
435
436 @Override protected void cancel() {
437 OsmApi.getOsmApi().cancel();
438 uploadCancelled = true;
439 }
440 }
441
442
443 class UploadConfirmationHook implements UploadHook {
444
445 private JCheckBox cbUseAtomicUpload;
446
447 protected JPanel buildChangesetControlPanel() {
448 JPanel pnl = new JPanel();
449 pnl.setLayout(new FlowLayout(FlowLayout.LEFT));
450 pnl.add(cbUseAtomicUpload = new JCheckBox(tr("upload all changes in one request")));
451 cbUseAtomicUpload.setToolTipText(tr("Enable to upload all changes in one request, disable to use one request per changed primitive"));
452 boolean useAtomicUpload = Main.pref.getBoolean("osm-server.atomic-upload", true);
453 cbUseAtomicUpload.setSelected(useAtomicUpload);
454 cbUseAtomicUpload.setEnabled(OsmApi.getOsmApi().hasChangesetSupport());
455 return pnl;
456 }
457
458 public boolean checkUpload(Collection<OsmPrimitive> add, Collection<OsmPrimitive> update, Collection<OsmPrimitive> delete) {
459
460 JPanel p = new JPanel(new GridBagLayout());
461
462 OsmPrimitivRenderer renderer = new OsmPrimitivRenderer();
463
464 if (!add.isEmpty()) {
465 p.add(new JLabel(tr("Objects to add:")), GBC.eol());
466 JList l = new JList(add.toArray());
467 l.setCellRenderer(renderer);
468 l.setVisibleRowCount(l.getModel().getSize() < 6 ? l.getModel().getSize() : 10);
469 p.add(new JScrollPane(l), GBC.eol().fill());
470 }
471
472 if (!update.isEmpty()) {
473 p.add(new JLabel(tr("Objects to modify:")), GBC.eol());
474 JList l = new JList(update.toArray());
475 l.setCellRenderer(renderer);
476 l.setVisibleRowCount(l.getModel().getSize() < 6 ? l.getModel().getSize() : 10);
477 p.add(new JScrollPane(l), GBC.eol().fill());
478 }
479
480 if (!delete.isEmpty()) {
481 p.add(new JLabel(tr("Objects to delete:")), GBC.eol());
482 JList l = new JList(delete.toArray());
483 l.setCellRenderer(renderer);
484 l.setVisibleRowCount(l.getModel().getSize() < 6 ? l.getModel().getSize() : 10);
485 p.add(new JScrollPane(l), GBC.eol().fill());
486 }
487
488 p.add(new JLabel(tr("Provide a brief comment for the changes you are uploading:")), GBC.eol().insets(0, 5, 10, 3));
489 SuggestingJHistoryComboBox cmt = new SuggestingJHistoryComboBox();
490 List<String> cmtHistory = new LinkedList<String>(Main.pref.getCollection(HISTORY_KEY, new LinkedList<String>()));
491 cmt.setHistory(cmtHistory);
492 p.add(cmt, GBC.eol().fill(GBC.HORIZONTAL));
493 //final JTextField cmt = new JTextField(lastCommitComment);
494
495 // configuration options for atomic upload
496 p.add(buildChangesetControlPanel(), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
497
498 while(true) {
499 int result = new ExtendedDialog(Main.parent,
500 tr("Upload these changes?"),
501 p,
502 new String[] {tr("Upload Changes"), tr("Cancel")},
503 new String[] {"upload.png", "cancel.png"}).getValue();
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
525 class UploadDiffTask extends PleaseWaitRunnable {
526 private boolean uploadCancelled = false;
527 private Exception lastException = null;
528 private Collection <OsmPrimitive> toUpload;
529 private OsmServerWriter writer;
530
531 public UploadDiffTask(Collection <OsmPrimitive> toUpload) {
532 super(tr("Uploading"),false /* don't ignore exceptions */);
533 this.toUpload = toUpload;
534 }
535
536 @Override protected void realRun() throws SAXException, IOException {
537 writer = new OsmServerWriter();
538 try {
539 writer.uploadOsm(getCurrentDataSet().version, toUpload, progressMonitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
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
553 // we always clean the data, even in case of errors. It's possible the data was
554 // partially uploaded
555 //
556 getEditLayer().cleanupAfterUpload(writer.getProcessedPrimitives());
557 DataSet.fireSelectionChanged(getEditLayer().data.getSelected());
558 getEditLayer().fireDataChange();
559 if (lastException != null) {
560 handleFailedUpload(lastException);
561 }
562 }
563
564 @Override protected void cancel() {
565 uploadCancelled = true;
566 if (writer != null) {
567 writer.disconnectActiveConnection();
568 }
569 }
570 }
571}
Note: See TracBrowser for help on using the repository browser.