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

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

fix #11077 - Changeset with 50k/10k objects is not auto-closed

  • Property svn:eol-style set to native
File size: 16.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.Main;
17import org.openstreetmap.josm.data.APIDataSet;
18import org.openstreetmap.josm.data.osm.Changeset;
19import org.openstreetmap.josm.data.osm.ChangesetCache;
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.DefaultNameFormatter;
26import org.openstreetmap.josm.gui.HelpAwareOptionPane;
27import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
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.io.ChangesetClosedException;
33import org.openstreetmap.josm.io.OsmApi;
34import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
35import org.openstreetmap.josm.io.OsmServerWriter;
36import org.openstreetmap.josm.io.OsmTransferCanceledException;
37import org.openstreetmap.josm.io.OsmTransferException;
38import org.openstreetmap.josm.tools.ImageProvider;
39
40/**
41 * The task for uploading a collection of primitives.
42 * @since 2599
43 */
44public class UploadPrimitivesTask extends AbstractUploadTask {
45 private boolean uploadCanceled;
46 private Exception lastException;
47 private final APIDataSet toUpload;
48 private OsmServerWriter writer;
49 private final OsmDataLayer layer;
50 private Changeset changeset;
51 private final Set<IPrimitive> processedPrimitives;
52 private final UploadStrategySpecification strategy;
53
54 /**
55 * Creates the task
56 *
57 * @param strategy the upload strategy. Must not be null.
58 * @param layer the OSM data layer for which data is uploaded. Must not be null.
59 * @param toUpload the collection of primitives to upload. Set to the empty collection if null.
60 * @param changeset the changeset to use for uploading. Must not be null. changeset.getId()
61 * can be 0 in which case the upload task creates a new changeset
62 * @throws IllegalArgumentException if layer is null
63 * @throws IllegalArgumentException if toUpload is null
64 * @throws IllegalArgumentException if strategy is null
65 * @throws IllegalArgumentException if changeset is null
66 */
67 public UploadPrimitivesTask(UploadStrategySpecification strategy, OsmDataLayer layer, APIDataSet toUpload, Changeset changeset) {
68 super(tr("Uploading data for layer ''{0}''", layer.getName()), false /* don't ignore exceptions */);
69 ensureParameterNotNull(layer, "layer");
70 ensureParameterNotNull(strategy, "strategy");
71 ensureParameterNotNull(changeset, "changeset");
72 this.toUpload = toUpload;
73 this.layer = layer;
74 this.changeset = changeset;
75 this.strategy = strategy;
76 this.processedPrimitives = new HashSet<>();
77 }
78
79 protected MaxChangesetSizeExceededPolicy askMaxChangesetSizeExceedsPolicy() {
80 ButtonSpec[] specs = new ButtonSpec[] {
81 new ButtonSpec(
82 tr("Continue uploading"),
83 ImageProvider.get("upload"),
84 tr("Click to continue uploading to additional new changesets"),
85 null /* no specific help text */
86 ),
87 new ButtonSpec(
88 tr("Go back to Upload Dialog"),
89 ImageProvider.get("dialogs", "uploadproperties"),
90 tr("Click to return to the Upload Dialog"),
91 null /* no specific help text */
92 ),
93 new ButtonSpec(
94 tr("Abort"),
95 ImageProvider.get("cancel"),
96 tr("Click to abort uploading"),
97 null /* no specific help text */
98 )
99 };
100 int numObjectsToUploadLeft = toUpload.getSize() - processedPrimitives.size();
101 String msg1 = tr("The server reported that the current changeset was closed.<br>"
102 + "This is most likely because the changesets size exceeded the max. size<br>"
103 + "of {0} objects on the server ''{1}''.",
104 OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(),
105 OsmApi.getOsmApi().getBaseUrl()
106 );
107 String msg2 = trn(
108 "There is {0} object left to upload.",
109 "There are {0} objects left to upload.",
110 numObjectsToUploadLeft,
111 numObjectsToUploadLeft
112 );
113 String msg3 = tr(
114 "Click ''<strong>{0}</strong>'' to continue uploading to additional new changesets.<br>"
115 + "Click ''<strong>{1}</strong>'' to return to the upload dialog.<br>"
116 + "Click ''<strong>{2}</strong>'' to abort uploading and return to map editing.<br>",
117 specs[0].text,
118 specs[1].text,
119 specs[2].text
120 );
121 String msg = "<html>" + msg1 + "<br><br>" + msg2 +"<br><br>" + msg3 + "</html>";
122 int ret = HelpAwareOptionPane.showOptionDialog(
123 Main.parent,
124 msg,
125 tr("Changeset is full"),
126 JOptionPane.WARNING_MESSAGE,
127 null, /* no special icon */
128 specs,
129 specs[0],
130 ht("/Action/Upload#ChangesetFull")
131 );
132 switch(ret) {
133 case 0: return MaxChangesetSizeExceededPolicy.AUTOMATICALLY_OPEN_NEW_CHANGESETS;
134 case 1: return MaxChangesetSizeExceededPolicy.FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG;
135 case 2:
136 case JOptionPane.CLOSED_OPTION:
137 default: return MaxChangesetSizeExceededPolicy.ABORT;
138 }
139 }
140
141 /**
142 * Opens a new changeset.
143 */
144 protected void openNewChangeset() {
145 // make sure the current changeset is removed from the upload dialog.
146 ChangesetCache.getInstance().update(changeset);
147 Changeset newChangeSet = new Changeset();
148 newChangeSet.setKeys(this.changeset.getKeys());
149 this.changeset = newChangeSet;
150 }
151
152 protected boolean recoverFromChangesetFullException() throws OsmTransferException {
153 if (toUpload.getSize() - processedPrimitives.size() == 0) {
154 strategy.setPolicy(MaxChangesetSizeExceededPolicy.ABORT);
155 return false;
156 }
157 if (strategy.getPolicy() == null || strategy.getPolicy().equals(MaxChangesetSizeExceededPolicy.ABORT)) {
158 strategy.setPolicy(askMaxChangesetSizeExceedsPolicy());
159 }
160 switch(strategy.getPolicy()) {
161 case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
162 // prepare the state of the task for a next iteration in uploading.
163 closeChangesetIfRequired();
164 openNewChangeset();
165 toUpload.removeProcessed(processedPrimitives);
166 return true;
167 case ABORT:
168 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
169 default:
170 // don't continue - finish() will send the user back to map editing or upload dialog
171 return false;
172 }
173 }
174
175 /**
176 * Retries to recover the upload operation from an exception which was thrown because
177 * an uploaded primitive was already deleted on the server.
178 *
179 * @param e the exception throw by the API
180 * @param monitor a progress monitor
181 * @throws OsmTransferException if we can't recover from the exception
182 */
183 protected void recoverFromGoneOnServer(OsmApiPrimitiveGoneException e, ProgressMonitor monitor) throws OsmTransferException {
184 if (!e.isKnownPrimitive()) throw e;
185 OsmPrimitive p = layer.data.getPrimitiveById(e.getPrimitiveId(), e.getPrimitiveType());
186 if (p == null) throw e;
187 if (p.isDeleted()) {
188 // we tried to delete an already deleted primitive.
189 final String msg;
190 final String displayName = p.getDisplayName(DefaultNameFormatter.getInstance());
191 if (p instanceof Node) {
192 msg = tr("Node ''{0}'' is already deleted. Skipping object in upload.", displayName);
193 } else if (p instanceof Way) {
194 msg = tr("Way ''{0}'' is already deleted. Skipping object in upload.", displayName);
195 } else if (p instanceof Relation) {
196 msg = tr("Relation ''{0}'' is already deleted. Skipping object in upload.", displayName);
197 } else {
198 msg = tr("Object ''{0}'' is already deleted. Skipping object in upload.", displayName);
199 }
200 monitor.appendLogMessage(msg);
201 Main.warn(msg);
202 processedPrimitives.addAll(writer.getProcessedPrimitives());
203 processedPrimitives.add(p);
204 toUpload.removeProcessed(processedPrimitives);
205 return;
206 }
207 // exception was thrown because we tried to *update* an already deleted
208 // primitive. We can't resolve this automatically. Re-throw exception,
209 // a conflict is going to be created later.
210 throw e;
211 }
212
213 protected void cleanupAfterUpload() {
214 // we always clean up the data, even in case of errors. It's possible the data was
215 // partially uploaded. Better run on EDT.
216 Runnable r = () -> {
217 layer.cleanupAfterUpload(processedPrimitives);
218 layer.onPostUploadToServer();
219 ChangesetCache.getInstance().update(changeset);
220 };
221
222 try {
223 SwingUtilities.invokeAndWait(r);
224 } catch (InterruptedException e) {
225 Main.trace(e);
226 lastException = e;
227 Thread.currentThread().interrupt();
228 } catch (InvocationTargetException e) {
229 Main.trace(e);
230 lastException = new OsmTransferException(e.getCause());
231 }
232 }
233
234 @Override
235 protected void realRun() {
236 try {
237 uploadloop: while (true) {
238 try {
239 getProgressMonitor().subTask(
240 trn("Uploading {0} object...", "Uploading {0} objects...", toUpload.getSize(), toUpload.getSize()));
241 synchronized (this) {
242 writer = new OsmServerWriter();
243 }
244 writer.uploadOsm(strategy, toUpload.getPrimitives(), changeset, getProgressMonitor().createSubTaskMonitor(1, false));
245
246 // if we get here we've successfully uploaded the data. Exit the loop.
247 break;
248 } catch (OsmTransferCanceledException e) {
249 Main.error(e);
250 uploadCanceled = true;
251 break uploadloop;
252 } catch (OsmApiPrimitiveGoneException e) {
253 // try to recover from 410 Gone
254 recoverFromGoneOnServer(e, getProgressMonitor());
255 } catch (ChangesetClosedException e) {
256 if (writer != null) {
257 processedPrimitives.addAll(writer.getProcessedPrimitives()); // OsmPrimitive in => OsmPrimitive out
258 }
259 switch(e.getSource()) {
260 case UPLOAD_DATA:
261 // Most likely the changeset is full. Try to recover and continue
262 // with a new changeset, but let the user decide first (see
263 // recoverFromChangesetFullException)
264 if (recoverFromChangesetFullException()) {
265 continue;
266 }
267 lastException = e;
268 break uploadloop;
269 case UNSPECIFIED:
270 case UPDATE_CHANGESET:
271 default:
272 // The changeset was closed when we tried to update it. Probably, our
273 // local list of open changesets got out of sync with the server state.
274 // The user will have to select another open changeset.
275 // Rethrow exception - this will be handled later.
276 changeset.setOpen(false);
277 throw e;
278 }
279 } finally {
280 if (writer != null) {
281 processedPrimitives.addAll(writer.getProcessedPrimitives());
282 }
283 synchronized (this) {
284 writer = null;
285 }
286 }
287 }
288 // if required close the changeset
289 closeChangesetIfRequired();
290 } catch (OsmTransferException e) {
291 if (uploadCanceled) {
292 Main.info(tr("Ignoring caught exception because upload is canceled. Exception is: {0}", e.toString()));
293 } else {
294 lastException = e;
295 }
296 }
297 if (uploadCanceled && processedPrimitives.isEmpty()) return;
298 cleanupAfterUpload();
299 }
300
301 private void closeChangesetIfRequired() throws OsmTransferException {
302 if (strategy.isCloseChangesetAfterUpload() && changeset != null && !changeset.isNew() && changeset.isOpen()) {
303 OsmApi.getOsmApi().closeChangeset(changeset, progressMonitor.createSubTaskMonitor(0, false));
304 }
305 }
306
307 @Override protected void finish() {
308
309 // depending on the success of the upload operation and on the policy for
310 // multi changeset uploads this will sent the user back to the appropriate
311 // place in JOSM, either
312 // - to an error dialog
313 // - to the Upload Dialog
314 // - to map editing
315 GuiHelper.runInEDT(() -> {
316 // if the changeset is still open after this upload we want it to be selected on the next upload
317 ChangesetCache.getInstance().update(changeset);
318 if (changeset != null && changeset.isOpen()) {
319 UploadDialog.getUploadDialog().setSelectedChangesetForNextUpload(changeset);
320 }
321 if (uploadCanceled) return;
322 if (lastException == null) {
323 new Notification(
324 "<h3>" + tr("Upload successful!") + "</h3>")
325 .setIcon(ImageProvider.get("misc", "check_large"))
326 .show();
327 return;
328 }
329 if (lastException instanceof ChangesetClosedException) {
330 ChangesetClosedException e = (ChangesetClosedException) lastException;
331 if (e.getSource().equals(ChangesetClosedException.Source.UPDATE_CHANGESET)) {
332 handleFailedUpload(lastException);
333 return;
334 }
335 if (strategy.getPolicy() == null)
336 /* do nothing if unknown policy */
337 return;
338 if (e.getSource().equals(ChangesetClosedException.Source.UPLOAD_DATA)) {
339 switch(strategy.getPolicy()) {
340 case ABORT:
341 break; /* do nothing - we return to map editing */
342 case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
343 break; /* do nothing - we return to map editing */
344 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
345 // return to the upload dialog
346 //
347 toUpload.removeProcessed(processedPrimitives);
348 UploadDialog.getUploadDialog().setUploadedPrimitives(toUpload);
349 UploadDialog.getUploadDialog().setVisible(true);
350 break;
351 }
352 } else {
353 handleFailedUpload(lastException);
354 }
355 } else {
356 handleFailedUpload(lastException);
357 }
358 });
359 }
360
361 @Override protected void cancel() {
362 uploadCanceled = true;
363 synchronized (this) {
364 if (writer != null) {
365 writer.cancel();
366 }
367 }
368 }
369}
Note: See TracBrowser for help on using the repository browser.