source: josm/trunk/src/org/openstreetmap/josm/gui/io/UploadPrimitivesTask.java@ 18283

Last change on this file since 18283 was 18283, checked in by Don-vip, 3 years ago

fix #21427 - further simplify UploadDialog (patch by marcello, modified)

  • The dialog was simplified by combining the function of two radiobuttons and one combobox into one combobox.
  • When an open changeset was selected on tab 2, existing tags on the open changeset could overwrite the data the user entered on tab 1. The user might spot this by looking closely at the tag table on tab 2, but then he may not. This non-obvious behaviour was removed.
  • The exception thrown when closing an already closed changeset was fixed.
  • More cosmetic changes to the dialog.
  • Maybe also a solution to #19319, #21387 (added revalidate()).
  • Property svn:eol-style set to native
File size: 18.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.io;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.CheckParameterUtil.ensureParameterNotNull;
6import static org.openstreetmap.josm.tools.I18n.tr;
7import static org.openstreetmap.josm.tools.I18n.trn;
8
9import java.lang.reflect.InvocationTargetException;
10import java.util.HashSet;
11import java.util.Set;
12
13import javax.swing.JOptionPane;
14import javax.swing.SwingUtilities;
15
16import org.openstreetmap.josm.data.APIDataSet;
17import org.openstreetmap.josm.data.osm.Changeset;
18import org.openstreetmap.josm.data.osm.ChangesetCache;
19import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
20import org.openstreetmap.josm.data.osm.IPrimitive;
21import org.openstreetmap.josm.data.osm.Node;
22import org.openstreetmap.josm.data.osm.OsmPrimitive;
23import org.openstreetmap.josm.data.osm.Relation;
24import org.openstreetmap.josm.data.osm.Way;
25import org.openstreetmap.josm.gui.HelpAwareOptionPane;
26import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
27import org.openstreetmap.josm.gui.MainApplication;
28import org.openstreetmap.josm.gui.Notification;
29import org.openstreetmap.josm.gui.layer.OsmDataLayer;
30import org.openstreetmap.josm.gui.progress.ProgressMonitor;
31import org.openstreetmap.josm.gui.util.GuiHelper;
32import org.openstreetmap.josm.gui.widgets.HtmlPanel;
33import org.openstreetmap.josm.io.ChangesetClosedException;
34import org.openstreetmap.josm.io.MaxChangesetSizeExceededPolicy;
35import org.openstreetmap.josm.io.MessageNotifier;
36import org.openstreetmap.josm.io.OsmApi;
37import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
38import org.openstreetmap.josm.io.OsmServerWriter;
39import org.openstreetmap.josm.io.OsmTransferCanceledException;
40import org.openstreetmap.josm.io.OsmTransferException;
41import org.openstreetmap.josm.io.UploadStrategySpecification;
42import org.openstreetmap.josm.spi.preferences.Config;
43import org.openstreetmap.josm.tools.ImageProvider;
44import org.openstreetmap.josm.tools.Logging;
45
46/**
47 * The task for uploading a collection of primitives.
48 * @since 2599
49 */
50public class UploadPrimitivesTask extends AbstractUploadTask {
51 private boolean uploadCanceled;
52 private Exception lastException;
53 private final APIDataSet toUpload;
54 private OsmServerWriter writer;
55 private final OsmDataLayer layer;
56 private Changeset changeset;
57 private final Set<IPrimitive> processedPrimitives;
58 private final UploadStrategySpecification strategy;
59
60 /**
61 * Creates the task
62 *
63 * @param strategy the upload strategy. Must not be null.
64 * @param layer the OSM data layer for which data is uploaded. Must not be null.
65 * @param toUpload the collection of primitives to upload. Set to the empty collection if null.
66 * @param changeset the changeset to use for uploading. Must not be null. changeset.getId()
67 * can be 0 in which case the upload task creates a new changeset
68 * @throws IllegalArgumentException if layer is null
69 * @throws IllegalArgumentException if toUpload is null
70 * @throws IllegalArgumentException if strategy is null
71 * @throws IllegalArgumentException if changeset is null
72 */
73 public UploadPrimitivesTask(UploadStrategySpecification strategy, OsmDataLayer layer, APIDataSet toUpload, Changeset changeset) {
74 super(tr("Uploading data for layer ''{0}''", layer.getName()), false /* don't ignore exceptions */);
75 ensureParameterNotNull(layer, "layer");
76 ensureParameterNotNull(strategy, "strategy");
77 ensureParameterNotNull(changeset, "changeset");
78 this.toUpload = toUpload;
79 this.layer = layer;
80 this.changeset = changeset;
81 this.strategy = strategy;
82 this.processedPrimitives = new HashSet<>();
83 }
84
85 /**
86 * Prompt the user about how to proceed.
87 *
88 * @return the policy selected by the user
89 */
90 protected MaxChangesetSizeExceededPolicy promptUserForPolicy() {
91 ButtonSpec[] specs = {
92 new ButtonSpec(
93 tr("Continue uploading"),
94 new ImageProvider("upload"),
95 tr("Click to continue uploading to additional new changesets"),
96 null /* no specific help text */
97 ),
98 new ButtonSpec(
99 tr("Go back to Upload Dialog"),
100 new ImageProvider("preference"),
101 tr("Click to return to the Upload Dialog"),
102 null /* no specific help text */
103 ),
104 new ButtonSpec(
105 tr("Abort"),
106 new ImageProvider("cancel"),
107 tr("Click to abort uploading"),
108 null /* no specific help text */
109 )
110 };
111 int numObjectsToUploadLeft = toUpload.getSize() - processedPrimitives.size();
112 String msg1 = tr("The server reported that the current changeset was closed.<br>"
113 + "This is most likely because the changesets size exceeded the max. size<br>"
114 + "of {0} objects on the server ''{1}''.",
115 OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(),
116 OsmApi.getOsmApi().getBaseUrl()
117 );
118 String msg2 = trn(
119 "There is {0} object left to upload.",
120 "There are {0} objects left to upload.",
121 numObjectsToUploadLeft,
122 numObjectsToUploadLeft
123 );
124 String msg3 = tr(
125 "Click ''<strong>{0}</strong>'' to continue uploading to additional new changesets.<br>"
126 + "Click ''<strong>{1}</strong>'' to return to the upload dialog.<br>"
127 + "Click ''<strong>{2}</strong>'' to abort uploading and return to map editing.<br>",
128 specs[0].text,
129 specs[1].text,
130 specs[2].text
131 );
132 String msg = "<html>" + msg1 + "<br><br>" + msg2 +"<br><br>" + msg3 + "</html>";
133 int ret = HelpAwareOptionPane.showOptionDialog(
134 MainApplication.getMainFrame(),
135 msg,
136 tr("Changeset is full"),
137 JOptionPane.WARNING_MESSAGE,
138 null, /* no special icon */
139 specs,
140 specs[0],
141 ht("/Action/Upload#ChangesetFull")
142 );
143 switch(ret) {
144 case 0: return MaxChangesetSizeExceededPolicy.AUTOMATICALLY_OPEN_NEW_CHANGESETS;
145 case 1: return MaxChangesetSizeExceededPolicy.FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG;
146 case 2:
147 case JOptionPane.CLOSED_OPTION:
148 default: return MaxChangesetSizeExceededPolicy.ABORT;
149 }
150 }
151
152 /**
153 * Handles a server changeset full response.
154 * <p>
155 * Handles a server changeset full response by either aborting or opening a new changeset, if the
156 * user requested it so.
157 *
158 * @return true if the upload process should continue with the new changeset, false if the
159 * upload should be interrupted
160 * @throws OsmTransferException "if something goes wrong."
161 */
162 protected boolean handleChangesetFullResponse() throws OsmTransferException {
163 if (processedPrimitives.size() == toUpload.getSize()) {
164 strategy.setPolicy(MaxChangesetSizeExceededPolicy.ABORT);
165 return false;
166 }
167 if (strategy.getPolicy() == null || strategy.getPolicy() == MaxChangesetSizeExceededPolicy.ABORT) {
168 strategy.setPolicy(promptUserForPolicy());
169 }
170 switch(strategy.getPolicy()) {
171 case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
172 Changeset newChangeSet = new Changeset();
173 newChangeSet.setKeys(changeset.getKeys());
174 closeChangeset();
175 this.changeset = newChangeSet;
176 toUpload.removeProcessed(processedPrimitives);
177 return true;
178 case ABORT:
179 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
180 default:
181 // don't continue - finish() will send the user back to map editing or upload dialog
182 return false;
183 }
184 }
185
186 /**
187 * Retries to recover the upload operation from an exception which was thrown because
188 * an uploaded primitive was already deleted on the server.
189 *
190 * @param e the exception throw by the API
191 * @param monitor a progress monitor
192 * @throws OsmTransferException if we can't recover from the exception
193 */
194 protected void recoverFromGoneOnServer(OsmApiPrimitiveGoneException e, ProgressMonitor monitor) throws OsmTransferException {
195 if (!e.isKnownPrimitive()) throw e;
196 OsmPrimitive p = layer.data.getPrimitiveById(e.getPrimitiveId(), e.getPrimitiveType());
197 if (p == null) throw e;
198 if (p.isDeleted()) {
199 // we tried to delete an already deleted primitive.
200 final String msg;
201 final String displayName = p.getDisplayName(DefaultNameFormatter.getInstance());
202 if (p instanceof Node) {
203 msg = tr("Node ''{0}'' is already deleted. Skipping object in upload.", displayName);
204 } else if (p instanceof Way) {
205 msg = tr("Way ''{0}'' is already deleted. Skipping object in upload.", displayName);
206 } else if (p instanceof Relation) {
207 msg = tr("Relation ''{0}'' is already deleted. Skipping object in upload.", displayName);
208 } else {
209 msg = tr("Object ''{0}'' is already deleted. Skipping object in upload.", displayName);
210 }
211 monitor.appendLogMessage(msg);
212 Logging.warn(msg);
213 processedPrimitives.addAll(writer.getProcessedPrimitives());
214 processedPrimitives.add(p);
215 toUpload.removeProcessed(processedPrimitives);
216 return;
217 }
218 // exception was thrown because we tried to *update* an already deleted
219 // primitive. We can't resolve this automatically. Re-throw exception,
220 // a conflict is going to be created later.
221 throw e;
222 }
223
224 protected void cleanupAfterUpload() {
225 // we always clean up the data, even in case of errors. It's possible the data was
226 // partially uploaded. Better run on EDT.
227 Runnable r = () -> {
228 boolean readOnly = layer.isLocked();
229 if (readOnly) {
230 layer.unlock();
231 }
232 try {
233 layer.cleanupAfterUpload(processedPrimitives);
234 layer.onPostUploadToServer();
235 ChangesetCache.getInstance().update(changeset);
236 } finally {
237 if (readOnly) {
238 layer.lock();
239 }
240 }
241 };
242
243 try {
244 SwingUtilities.invokeAndWait(r);
245 } catch (InterruptedException e) {
246 Logging.trace(e);
247 lastException = e;
248 Thread.currentThread().interrupt();
249 } catch (InvocationTargetException e) {
250 Logging.trace(e);
251 lastException = new OsmTransferException(e.getCause());
252 }
253 }
254
255 @Override
256 protected void realRun() {
257 try {
258 MessageNotifier.stop();
259 uploadloop: while (true) {
260 try {
261 getProgressMonitor().subTask(
262 trn("Uploading {0} object...", "Uploading {0} objects...", toUpload.getSize(), toUpload.getSize()));
263 synchronized (this) {
264 writer = new OsmServerWriter();
265 }
266 writer.uploadOsm(strategy, toUpload.getPrimitives(), changeset, getProgressMonitor().createSubTaskMonitor(1, false));
267 // If the changeset was new, now it is open.
268 ChangesetCache.getInstance().update(changeset);
269 // if we get here we've successfully uploaded the data. Exit the loop.
270 break;
271 } catch (OsmTransferCanceledException e) {
272 Logging.error(e);
273 uploadCanceled = true;
274 break uploadloop;
275 } catch (OsmApiPrimitiveGoneException e) {
276 // try to recover from 410 Gone
277 recoverFromGoneOnServer(e, getProgressMonitor());
278 } catch (ChangesetClosedException e) {
279 if (writer != null) {
280 processedPrimitives.addAll(writer.getProcessedPrimitives()); // OsmPrimitive in => OsmPrimitive out
281 }
282 switch(e.getSource()) {
283 case UPLOAD_DATA:
284 // Most likely the changeset is full. Try to recover and continue
285 // with a new changeset, but let the user decide first.
286 if (handleChangesetFullResponse()) {
287 continue;
288 }
289 lastException = e;
290 break uploadloop;
291 case UPDATE_CHANGESET:
292 case CLOSE_CHANGESET:
293 case UNSPECIFIED:
294 default:
295 // The changeset was closed when we tried to update it. Probably, our
296 // local list of open changesets got out of sync with the server state.
297 // The user will have to select another open changeset.
298 // Rethrow exception - this will be handled later.
299 changeset.setOpen(false);
300 ChangesetCache.getInstance().update(changeset);
301 throw e;
302 }
303 } finally {
304 if (writer != null) {
305 processedPrimitives.addAll(writer.getProcessedPrimitives());
306 }
307 synchronized (this) {
308 writer = null;
309 }
310 }
311 }
312 // if required close the changeset
313 closeChangesetIfRequired();
314 } catch (OsmTransferException e) {
315 if (uploadCanceled) {
316 Logging.info(tr("Ignoring caught exception because upload is canceled. Exception is: {0}", e.toString()));
317 } else {
318 lastException = e;
319 }
320 } finally {
321 if (MessageNotifier.PROP_NOTIFIER_ENABLED.get()) {
322 MessageNotifier.start();
323 }
324 }
325 if (uploadCanceled && processedPrimitives.isEmpty()) return;
326 cleanupAfterUpload();
327 }
328
329 /**
330 * Closes the changeset on the server and locally.
331 *
332 * @throws OsmTransferException "if something goes wrong."
333 */
334 private void closeChangeset() throws OsmTransferException {
335 if (changeset != null && !changeset.isNew() && changeset.isOpen()) {
336 try {
337 OsmApi.getOsmApi().closeChangeset(changeset, progressMonitor.createSubTaskMonitor(0, false));
338 } catch (ChangesetClosedException e) {
339 // Do not raise a stink, probably the changeset timed out.
340 Logging.trace(e);
341 } finally {
342 changeset.setOpen(false);
343 ChangesetCache.getInstance().update(changeset);
344 }
345 }
346 }
347
348 private void closeChangesetIfRequired() throws OsmTransferException {
349 if (strategy.isCloseChangesetAfterUpload()) {
350 closeChangeset();
351 }
352 }
353
354 /**
355 * Depending on the success of the upload operation and on the policy for
356 * multi changeset uploads this will send the user back to the appropriate
357 * place in JOSM, either:
358 * <ul>
359 * <li>to an error dialog,
360 * <li>to the Upload Dialog, or
361 * <li>to map editing.
362 * </ul>
363 */
364 @Override
365 protected void finish() {
366 GuiHelper.runInEDT(() -> {
367 // if the changeset is still open after this upload we want it to be selected on the next upload
368 ChangesetCache.getInstance().update(changeset);
369 if (changeset != null && changeset.isOpen()) {
370 UploadDialog.getUploadDialog().setSelectedChangesetForNextUpload(changeset);
371 }
372 if (uploadCanceled) return;
373 if (lastException == null) {
374 HtmlPanel panel = new HtmlPanel(
375 "<h3><a href=\"" + Config.getUrls().getBaseBrowseUrl() + "/changeset/" + changeset.getId() + "\">"
376 + tr("Upload successful!") + "</a></h3>");
377 panel.enableClickableHyperlinks();
378 panel.setOpaque(false);
379 new Notification()
380 .setContent(panel)
381 .setIcon(ImageProvider.get("misc", "check_large"))
382 .show();
383 return;
384 }
385 if (lastException instanceof ChangesetClosedException) {
386 ChangesetClosedException e = (ChangesetClosedException) lastException;
387 if (e.getSource() == ChangesetClosedException.Source.UPDATE_CHANGESET) {
388 handleFailedUpload(lastException);
389 return;
390 }
391 if (strategy.getPolicy() == null)
392 /* do nothing if unknown policy */
393 return;
394 if (e.getSource() == ChangesetClosedException.Source.UPLOAD_DATA) {
395 switch(strategy.getPolicy()) {
396 case ABORT:
397 break; /* do nothing - we return to map editing */
398 case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
399 break; /* do nothing - we return to map editing */
400 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
401 // return to the upload dialog
402 //
403 toUpload.removeProcessed(processedPrimitives);
404 UploadDialog.getUploadDialog().setUploadedPrimitives(toUpload);
405 UploadDialog.getUploadDialog().setVisible(true);
406 break;
407 }
408 } else {
409 handleFailedUpload(lastException);
410 }
411 } else {
412 handleFailedUpload(lastException);
413 }
414 });
415 }
416
417 @Override protected void cancel() {
418 uploadCanceled = true;
419 synchronized (this) {
420 if (writer != null) {
421 writer.cancel();
422 }
423 }
424 }
425}
Note: See TracBrowser for help on using the repository browser.