001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.io; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.CheckParameterUtil.ensureParameterNotNull; 006import static org.openstreetmap.josm.tools.I18n.tr; 007import static org.openstreetmap.josm.tools.I18n.trn; 008 009import java.lang.reflect.InvocationTargetException; 010import java.util.HashSet; 011import java.util.Set; 012 013import javax.swing.JOptionPane; 014import javax.swing.SwingUtilities; 015 016import org.openstreetmap.josm.Main; 017import org.openstreetmap.josm.data.APIDataSet; 018import org.openstreetmap.josm.data.osm.Changeset; 019import org.openstreetmap.josm.data.osm.ChangesetCache; 020import org.openstreetmap.josm.data.osm.IPrimitive; 021import org.openstreetmap.josm.data.osm.Node; 022import org.openstreetmap.josm.data.osm.OsmPrimitive; 023import org.openstreetmap.josm.data.osm.Relation; 024import org.openstreetmap.josm.data.osm.Way; 025import org.openstreetmap.josm.gui.DefaultNameFormatter; 026import org.openstreetmap.josm.gui.HelpAwareOptionPane; 027import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 028import org.openstreetmap.josm.gui.Notification; 029import org.openstreetmap.josm.gui.layer.OsmDataLayer; 030import org.openstreetmap.josm.gui.progress.ProgressMonitor; 031import org.openstreetmap.josm.gui.util.GuiHelper; 032import org.openstreetmap.josm.io.ChangesetClosedException; 033import org.openstreetmap.josm.io.OsmApi; 034import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException; 035import org.openstreetmap.josm.io.OsmServerWriter; 036import org.openstreetmap.josm.io.OsmTransferCanceledException; 037import org.openstreetmap.josm.io.OsmTransferException; 038import org.openstreetmap.josm.tools.ImageProvider; 039 040/** 041 * The task for uploading a collection of primitives. 042 * 043 */ 044public class UploadPrimitivesTask extends AbstractUploadTask { 045 private boolean uploadCanceled; 046 private Exception lastException; 047 private final APIDataSet toUpload; 048 private OsmServerWriter writer; 049 private final OsmDataLayer layer; 050 private Changeset changeset; 051 private final Set<IPrimitive> processedPrimitives; 052 private final 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 if layer is null 063 * @throws IllegalArgumentException if toUpload is null 064 * @throws IllegalArgumentException if strategy is null 065 * @throws IllegalArgumentException 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<>(); 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 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 Main.warn(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 @Override 224 public void run() { 225 layer.cleanupAfterUpload(processedPrimitives); 226 layer.onPostUploadToServer(); 227 ChangesetCache.getInstance().update(changeset); 228 } 229 }; 230 231 try { 232 SwingUtilities.invokeAndWait(r); 233 } catch (InterruptedException e) { 234 lastException = e; 235 } catch (InvocationTargetException e) { 236 lastException = new OsmTransferException(e.getCause()); 237 } 238 } 239 240 @Override 241 protected void realRun() { 242 try { 243 uploadloop: while (true) { 244 try { 245 getProgressMonitor().subTask( 246 trn("Uploading {0} object...", "Uploading {0} objects...", toUpload.getSize(), toUpload.getSize())); 247 synchronized (this) { 248 writer = new OsmServerWriter(); 249 } 250 writer.uploadOsm(strategy, toUpload.getPrimitives(), changeset, getProgressMonitor().createSubTaskMonitor(1, false)); 251 252 // if we get here we've successfully uploaded the data. Exit the loop. 253 // 254 break; 255 } catch (OsmTransferCanceledException e) { 256 Main.error(e); 257 uploadCanceled = true; 258 break uploadloop; 259 } catch (OsmApiPrimitiveGoneException e) { 260 // try to recover from 410 Gone 261 // 262 recoverFromGoneOnServer(e, getProgressMonitor()); 263 } catch (ChangesetClosedException e) { 264 processedPrimitives.addAll(writer.getProcessedPrimitives()); // OsmPrimitive in => OsmPrimitive out 265 changeset.setOpen(false); 266 switch(e.getSource()) { 267 case UNSPECIFIED: 268 throw e; 269 case UPDATE_CHANGESET: 270 // The changeset was closed when we tried to update it. Probably, our 271 // local list of open changesets got out of sync with the server state. 272 // The user will have to select another open changeset. 273 // Rethrow exception - this will be handled later. 274 // 275 throw e; 276 case UPLOAD_DATA: 277 // Most likely the changeset is full. Try to recover and continue 278 // with a new changeset, but let the user decide first (see 279 // recoverFromChangesetFullException) 280 // 281 if (recoverFromChangesetFullException()) { 282 continue; 283 } 284 lastException = e; 285 break uploadloop; 286 } 287 } finally { 288 if (writer != null) { 289 processedPrimitives.addAll(writer.getProcessedPrimitives()); 290 } 291 synchronized (this) { 292 writer = null; 293 } 294 } 295 } 296 // if required close the changeset 297 // 298 if (strategy.isCloseChangesetAfterUpload() && changeset != null && !changeset.isNew() && changeset.isOpen()) { 299 OsmApi.getOsmApi().closeChangeset(changeset, progressMonitor.createSubTaskMonitor(0, false)); 300 } 301 } catch (OsmTransferException e) { 302 if (uploadCanceled) { 303 Main.info(tr("Ignoring caught exception because upload is canceled. Exception is: {0}", e.toString())); 304 } else { 305 lastException = e; 306 } 307 } 308 if (uploadCanceled && processedPrimitives.isEmpty()) return; 309 cleanupAfterUpload(); 310 } 311 312 @Override protected void finish() { 313 314 // depending on the success of the upload operation and on the policy for 315 // multi changeset uploads this will sent the user back to the appropriate 316 // place in JOSM, either 317 // - to an error dialog 318 // - to the Upload Dialog 319 // - to map editing 320 GuiHelper.runInEDT(new Runnable() { 321 @Override 322 public void run() { 323 // if the changeset is still open after this upload we want it to 324 // be selected on the next upload 325 // 326 ChangesetCache.getInstance().update(changeset); 327 if (changeset != null && changeset.isOpen()) { 328 UploadDialog.getUploadDialog().setSelectedChangesetForNextUpload(changeset); 329 } 330 if (uploadCanceled) return; 331 if (lastException == null) { 332 new Notification( 333 "<h3>" + tr("Upload successful!") + "</h3>") 334 .setIcon(ImageProvider.get("misc", "check_large")) 335 .show(); 336 return; 337 } 338 if (lastException instanceof ChangesetClosedException) { 339 ChangesetClosedException e = (ChangesetClosedException) lastException; 340 if (e.getSource().equals(ChangesetClosedException.Source.UPDATE_CHANGESET)) { 341 handleFailedUpload(lastException); 342 return; 343 } 344 if (strategy.getPolicy() == null) 345 /* do nothing if unknown policy */ 346 return; 347 if (e.getSource().equals(ChangesetClosedException.Source.UPLOAD_DATA)) { 348 switch(strategy.getPolicy()) { 349 case ABORT: 350 break; /* do nothing - we return to map editing */ 351 case AUTOMATICALLY_OPEN_NEW_CHANGESETS: 352 break; /* do nothing - we return to map editing */ 353 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG: 354 // return to the upload dialog 355 // 356 toUpload.removeProcessed(processedPrimitives); 357 UploadDialog.getUploadDialog().setUploadedPrimitives(toUpload); 358 UploadDialog.getUploadDialog().setVisible(true); 359 break; 360 } 361 } else { 362 handleFailedUpload(lastException); 363 } 364 } else { 365 handleFailedUpload(lastException); 366 } 367 } 368 }); 369 } 370 371 @Override protected void cancel() { 372 uploadCanceled = true; 373 synchronized (this) { 374 if (writer != null) { 375 writer.cancel(); 376 } 377 } 378 } 379}