001 // License: GPL. For details, see LICENSE file.
002 package org.openstreetmap.josm.gui.io;
003
004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005 import static org.openstreetmap.josm.tools.CheckParameterUtil.ensureParameterNotNull;
006 import static org.openstreetmap.josm.tools.I18n.tr;
007 import static org.openstreetmap.josm.tools.I18n.trn;
008
009 import java.io.IOException;
010 import java.lang.reflect.InvocationTargetException;
011 import java.util.HashSet;
012
013 import javax.swing.JOptionPane;
014 import javax.swing.SwingUtilities;
015
016 import org.openstreetmap.josm.Main;
017 import org.openstreetmap.josm.data.APIDataSet;
018 import org.openstreetmap.josm.data.osm.Changeset;
019 import org.openstreetmap.josm.data.osm.ChangesetCache;
020 import org.openstreetmap.josm.data.osm.IPrimitive;
021 import org.openstreetmap.josm.data.osm.Node;
022 import org.openstreetmap.josm.data.osm.OsmPrimitive;
023 import org.openstreetmap.josm.data.osm.Relation;
024 import org.openstreetmap.josm.data.osm.Way;
025 import org.openstreetmap.josm.gui.DefaultNameFormatter;
026 import org.openstreetmap.josm.gui.HelpAwareOptionPane;
027 import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
028 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
029 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
030 import org.openstreetmap.josm.gui.util.GuiHelper;
031 import org.openstreetmap.josm.io.ChangesetClosedException;
032 import org.openstreetmap.josm.io.OsmApi;
033 import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
034 import org.openstreetmap.josm.io.OsmServerWriter;
035 import org.openstreetmap.josm.io.OsmTransferCanceledException;
036 import org.openstreetmap.josm.io.OsmTransferException;
037 import org.openstreetmap.josm.tools.ImageProvider;
038 import org.xml.sax.SAXException;
039
040 /**
041 * The task for uploading a collection of primitives
042 *
043 */
044 public class UploadPrimitivesTask extends AbstractUploadTask {
045 private boolean uploadCanceled = false;
046 private Exception lastException = null;
047 private APIDataSet toUpload;
048 private OsmServerWriter writer;
049 private OsmDataLayer layer;
050 private Changeset changeset;
051 private HashSet<IPrimitive> processedPrimitives;
052 private UploadStrategySpecification strategy;
053
054 /**
055 * Creates the task
056 *
057 * @param strategy the upload strategy. Must not be null.
058 * @param layer the OSM data layer for which data is uploaded. Must not be null.
059 * @param toUpload the collection of primitives to upload. Set to the empty collection if null.
060 * @param changeset the changeset to use for uploading. Must not be null. changeset.getId()
061 * can be 0 in which case the upload task creates a new changeset
062 * @throws IllegalArgumentException thrown if layer is null
063 * @throws IllegalArgumentException thrown if toUpload is null
064 * @throws IllegalArgumentException thrown if strategy is null
065 * @throws IllegalArgumentException thrown if changeset is null
066 */
067 public UploadPrimitivesTask(UploadStrategySpecification strategy, OsmDataLayer layer, APIDataSet toUpload, Changeset changeset) {
068 super(tr("Uploading data for layer ''{0}''", layer.getName()),false /* don't ignore exceptions */);
069 ensureParameterNotNull(layer,"layer");
070 ensureParameterNotNull(strategy, "strategy");
071 ensureParameterNotNull(changeset, "changeset");
072 this.toUpload = toUpload;
073 this.layer = layer;
074 this.changeset = changeset;
075 this.strategy = strategy;
076 this.processedPrimitives = new HashSet<IPrimitive>();
077 }
078
079 protected MaxChangesetSizeExceededPolicy askMaxChangesetSizeExceedsPolicy() {
080 ButtonSpec[] specs = new ButtonSpec[] {
081 new ButtonSpec(
082 tr("Continue uploading"),
083 ImageProvider.get("upload"),
084 tr("Click to continue uploading to additional new changesets"),
085 null /* no specific help text */
086 ),
087 new ButtonSpec(
088 tr("Go back to Upload Dialog"),
089 ImageProvider.get("dialogs", "uploadproperties"),
090 tr("Click to return to the Upload Dialog"),
091 null /* no specific help text */
092 ),
093 new ButtonSpec(
094 tr("Abort"),
095 ImageProvider.get("cancel"),
096 tr("Click to abort uploading"),
097 null /* no specific help text */
098 )
099 };
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: return MaxChangesetSizeExceededPolicy.ABORT;
136 case JOptionPane.CLOSED_OPTION: return MaxChangesetSizeExceededPolicy.ABORT;
137 }
138 // should not happen
139 return null;
140 }
141
142 protected void openNewChangeset() {
143 // make sure the current changeset is removed from the upload dialog.
144 //
145 ChangesetCache.getInstance().update(changeset);
146 Changeset newChangeSet = new Changeset();
147 newChangeSet.setKeys(this.changeset.getKeys());
148 this.changeset = newChangeSet;
149 }
150
151 protected boolean recoverFromChangesetFullException() {
152 if (toUpload.getSize() - processedPrimitives.size() == 0) {
153 strategy.setPolicy(MaxChangesetSizeExceededPolicy.ABORT);
154 return false;
155 }
156 if (strategy.getPolicy() == null || strategy.getPolicy().equals(MaxChangesetSizeExceededPolicy.ABORT)) {
157 MaxChangesetSizeExceededPolicy policy = askMaxChangesetSizeExceedsPolicy();
158 strategy.setPolicy(policy);
159 }
160 switch(strategy.getPolicy()) {
161 case ABORT:
162 // don't continue - finish() will send the user back to map editing
163 //
164 return false;
165 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
166 // don't continue - finish() will send the user back to the upload dialog
167 //
168 return false;
169 case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
170 // prepare the state of the task for a next iteration in uploading.
171 //
172 openNewChangeset();
173 toUpload.removeProcessed(processedPrimitives);
174 return true;
175 }
176 // should not happen
177 return false;
178 }
179
180 /**
181 * Retries to recover the upload operation from an exception which was thrown because
182 * an uploaded primitive was already deleted on the server.
183 *
184 * @param e the exception throw by the API
185 * @param monitor a progress monitor
186 * @throws OsmTransferException thrown if we can't recover from the exception
187 */
188 protected void recoverFromGoneOnServer(OsmApiPrimitiveGoneException e, ProgressMonitor monitor) throws OsmTransferException{
189 if (!e.isKnownPrimitive()) throw e;
190 OsmPrimitive p = layer.data.getPrimitiveById(e.getPrimitiveId(), e.getPrimitiveType());
191 if (p == null) throw e;
192 if (p.isDeleted()) {
193 // we tried to delete an already deleted primitive.
194 final String msg;
195 final String displayName = p.getDisplayName(DefaultNameFormatter.getInstance());
196 if (p instanceof Node) {
197 msg = tr("Node ''{0}'' is already deleted. Skipping object in upload.", displayName);
198 } else if (p instanceof Way) {
199 msg = tr("Way ''{0}'' is already deleted. Skipping object in upload.", displayName);
200 } else if (p instanceof Relation) {
201 msg = tr("Relation ''{0}'' is already deleted. Skipping object in upload.", displayName);
202 } else {
203 msg = tr("Object ''{0}'' is already deleted. Skipping object in upload.", displayName);
204 }
205 monitor.appendLogMessage(msg);
206 System.out.println(tr("Warning: {0}", msg));
207 processedPrimitives.addAll(writer.getProcessedPrimitives());
208 processedPrimitives.add(p);
209 toUpload.removeProcessed(processedPrimitives);
210 return;
211 }
212 // exception was thrown because we tried to *update* an already deleted
213 // primitive. We can't resolve this automatically. Re-throw exception,
214 // a conflict is going to be created later.
215 throw e;
216 }
217
218 protected void cleanupAfterUpload() {
219 // we always clean up the data, even in case of errors. It's possible the data was
220 // partially uploaded. Better run on EDT.
221 //
222 Runnable r = new Runnable() {
223 public void run() {
224 layer.cleanupAfterUpload(processedPrimitives);
225 layer.onPostUploadToServer();
226 ChangesetCache.getInstance().update(changeset);
227 }
228 };
229
230 try {
231 SwingUtilities.invokeAndWait(r);
232 } catch(InterruptedException e) {
233 lastException = e;
234 } catch(InvocationTargetException e) {
235 lastException = new OsmTransferException(e.getCause());
236 }
237 }
238
239 @Override protected void realRun() throws SAXException, IOException {
240 try {
241 uploadloop:while(true) {
242 try {
243 getProgressMonitor().subTask(trn("Uploading {0} object...", "Uploading {0} objects...", toUpload.getSize(), toUpload.getSize()));
244 synchronized(this) {
245 writer = new OsmServerWriter();
246 }
247 writer.uploadOsm(strategy, toUpload.getPrimitives(), changeset, getProgressMonitor().createSubTaskMonitor(1, false));
248
249 // if we get here we've successfully uploaded the data. Exit the loop.
250 //
251 break;
252 } catch(OsmTransferCanceledException e) {
253 e.printStackTrace();
254 uploadCanceled = true;
255 break uploadloop;
256 } catch(OsmApiPrimitiveGoneException e) {
257 // try to recover from 410 Gone
258 //
259 recoverFromGoneOnServer(e, getProgressMonitor());
260 } catch(ChangesetClosedException e) {
261 processedPrimitives.addAll(writer.getProcessedPrimitives()); // OsmPrimitive in => OsmPrimitive out
262 changeset.setOpen(false);
263 switch(e.getSource()) {
264 case UNSPECIFIED:
265 throw e;
266 case UPDATE_CHANGESET:
267 // The changeset was closed when we tried to update it. Probably, our
268 // local list of open changesets got out of sync with the server state.
269 // The user will have to select another open changeset.
270 // Rethrow exception - this will be handled later.
271 //
272 throw e;
273 case UPLOAD_DATA:
274 // Most likely the changeset is full. Try to recover and continue
275 // with a new changeset, but let the user decide first (see
276 // recoverFromChangesetFullException)
277 //
278 if (recoverFromChangesetFullException()) {
279 continue;
280 }
281 lastException = e;
282 break uploadloop;
283 }
284 } finally {
285 if (writer != null) {
286 processedPrimitives.addAll(writer.getProcessedPrimitives());
287 }
288 synchronized(this) {
289 writer = null;
290 }
291 }
292 }
293 // if required close the changeset
294 //
295 if (strategy.isCloseChangesetAfterUpload() && changeset != null && !changeset.isNew() && changeset.isOpen()) {
296 OsmApi.getOsmApi().closeChangeset(changeset, progressMonitor.createSubTaskMonitor(0, false));
297 }
298 } catch (Exception e) {
299 if (uploadCanceled) {
300 System.out.println(tr("Ignoring caught exception because upload is canceled. Exception is: {0}", e.toString()));
301 } else {
302 lastException = e;
303 }
304 }
305 if (uploadCanceled && processedPrimitives.isEmpty()) return;
306 cleanupAfterUpload();
307 }
308
309 @Override protected void finish() {
310 if (uploadCanceled)
311 return;
312
313 // depending on the success of the upload operation and on the policy for
314 // multi changeset uploads this will sent the user back to the appropriate
315 // place in JOSM, either
316 // - to an error dialog
317 // - to the Upload Dialog
318 // - to map editing
319 GuiHelper.runInEDT(new Runnable() {
320 public void run() {
321 // if the changeset is still open after this upload we want it to
322 // be selected on the next upload
323 //
324 ChangesetCache.getInstance().update(changeset);
325 if (changeset != null && changeset.isOpen()) {
326 UploadDialog.getUploadDialog().setSelectedChangesetForNextUpload(changeset);
327 }
328 if (lastException == null)
329 return;
330 if (lastException instanceof ChangesetClosedException) {
331 ChangesetClosedException e = (ChangesetClosedException)lastException;
332 if (e.getSource().equals(ChangesetClosedException.Source.UPDATE_CHANGESET)) {
333 handleFailedUpload(lastException);
334 return;
335 }
336 if (strategy.getPolicy() == null)
337 /* do nothing if unknown policy */
338 return;
339 if (e.getSource().equals(ChangesetClosedException.Source.UPLOAD_DATA)) {
340 switch(strategy.getPolicy()) {
341 case ABORT:
342 break; /* do nothing - we return to map editing */
343 case AUTOMATICALLY_OPEN_NEW_CHANGESETS:
344 break; /* do nothing - we return to map editing */
345 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG:
346 // return to the upload dialog
347 //
348 toUpload.removeProcessed(processedPrimitives);
349 UploadDialog.getUploadDialog().setUploadedPrimitives(toUpload);
350 UploadDialog.getUploadDialog().setVisible(true);
351 break;
352 }
353 } else {
354 handleFailedUpload(lastException);
355 }
356 } else {
357 handleFailedUpload(lastException);
358 }
359 }
360 });
361 }
362
363 @Override protected void cancel() {
364 uploadCanceled = true;
365 synchronized(this) {
366 if (writer != null) {
367 writer.cancel();
368 }
369 }
370 }
371 }