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

Last change on this file was 19307, checked in by taylor.smock, 5 months ago

Fix most new PMD issues

It would be better to use the newer switch syntax introduced in Java 14 (JEP 361),
but we currently target Java 11+. When we move to Java 17, this should be
reverted and the newer switch syntax should be used.

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