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

Last change on this file was 19307, checked in by taylor.smock, 11 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.