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.io.IOException; 010import java.lang.reflect.InvocationTargetException; 011import java.util.HashSet; 012import java.util.Set; 013 014import javax.swing.JOptionPane; 015import javax.swing.SwingUtilities; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.data.APIDataSet; 019import org.openstreetmap.josm.data.osm.Changeset; 020import org.openstreetmap.josm.data.osm.ChangesetCache; 021import org.openstreetmap.josm.data.osm.IPrimitive; 022import org.openstreetmap.josm.data.osm.Node; 023import org.openstreetmap.josm.data.osm.OsmPrimitive; 024import org.openstreetmap.josm.data.osm.Relation; 025import org.openstreetmap.josm.data.osm.Way; 026import org.openstreetmap.josm.gui.DefaultNameFormatter; 027import org.openstreetmap.josm.gui.HelpAwareOptionPane; 028import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 029import org.openstreetmap.josm.gui.Notification; 030import org.openstreetmap.josm.gui.layer.OsmDataLayer; 031import org.openstreetmap.josm.gui.progress.ProgressMonitor; 032import org.openstreetmap.josm.gui.util.GuiHelper; 033import org.openstreetmap.josm.io.ChangesetClosedException; 034import org.openstreetmap.josm.io.OsmApi; 035import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException; 036import org.openstreetmap.josm.io.OsmServerWriter; 037import org.openstreetmap.josm.io.OsmTransferCanceledException; 038import org.openstreetmap.josm.io.OsmTransferException; 039import org.openstreetmap.josm.tools.ImageProvider; 040import org.xml.sax.SAXException; 041 042/** 043 * The task for uploading a collection of primitives 044 * 045 */ 046public class UploadPrimitivesTask extends AbstractUploadTask { 047 private boolean uploadCanceled = false; 048 private Exception lastException = null; 049 private APIDataSet toUpload; 050 private OsmServerWriter writer; 051 private OsmDataLayer layer; 052 private Changeset changeset; 053 private Set<IPrimitive> processedPrimitives; 054 private UploadStrategySpecification strategy; 055 056 /** 057 * Creates the task 058 * 059 * @param strategy the upload strategy. Must not be null. 060 * @param layer the OSM data layer for which data is uploaded. Must not be null. 061 * @param toUpload the collection of primitives to upload. Set to the empty collection if null. 062 * @param changeset the changeset to use for uploading. Must not be null. changeset.getId() 063 * can be 0 in which case the upload task creates a new changeset 064 * @throws IllegalArgumentException thrown if layer is null 065 * @throws IllegalArgumentException thrown if toUpload is null 066 * @throws IllegalArgumentException thrown if strategy is null 067 * @throws IllegalArgumentException thrown if changeset is null 068 */ 069 public UploadPrimitivesTask(UploadStrategySpecification strategy, OsmDataLayer layer, APIDataSet toUpload, Changeset changeset) { 070 super(tr("Uploading data for layer ''{0}''", layer.getName()),false /* don't ignore exceptions */); 071 ensureParameterNotNull(layer,"layer"); 072 ensureParameterNotNull(strategy, "strategy"); 073 ensureParameterNotNull(changeset, "changeset"); 074 this.toUpload = toUpload; 075 this.layer = layer; 076 this.changeset = changeset; 077 this.strategy = strategy; 078 this.processedPrimitives = new HashSet<IPrimitive>(); 079 } 080 081 protected MaxChangesetSizeExceededPolicy askMaxChangesetSizeExceedsPolicy() { 082 ButtonSpec[] specs = new ButtonSpec[] { 083 new ButtonSpec( 084 tr("Continue uploading"), 085 ImageProvider.get("upload"), 086 tr("Click to continue uploading to additional new changesets"), 087 null /* no specific help text */ 088 ), 089 new ButtonSpec( 090 tr("Go back to Upload Dialog"), 091 ImageProvider.get("dialogs", "uploadproperties"), 092 tr("Click to return to the Upload Dialog"), 093 null /* no specific help text */ 094 ), 095 new ButtonSpec( 096 tr("Abort"), 097 ImageProvider.get("cancel"), 098 tr("Click to abort uploading"), 099 null /* no specific help text */ 100 ) 101 }; 102 int numObjectsToUploadLeft = toUpload.getSize() - processedPrimitives.size(); 103 String msg1 = tr("The server reported that the current changeset was closed.<br>" 104 + "This is most likely because the changesets size exceeded the max. size<br>" 105 + "of {0} objects on the server ''{1}''.", 106 OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(), 107 OsmApi.getOsmApi().getBaseUrl() 108 ); 109 String msg2 = trn( 110 "There is {0} object left to upload.", 111 "There are {0} objects left to upload.", 112 numObjectsToUploadLeft, 113 numObjectsToUploadLeft 114 ); 115 String msg3 = tr( 116 "Click ''<strong>{0}</strong>'' to continue uploading to additional new changesets.<br>" 117 + "Click ''<strong>{1}</strong>'' to return to the upload dialog.<br>" 118 + "Click ''<strong>{2}</strong>'' to abort uploading and return to map editing.<br>", 119 specs[0].text, 120 specs[1].text, 121 specs[2].text 122 ); 123 String msg = "<html>" + msg1 + "<br><br>" + msg2 +"<br><br>" + msg3 + "</html>"; 124 int ret = HelpAwareOptionPane.showOptionDialog( 125 Main.parent, 126 msg, 127 tr("Changeset is full"), 128 JOptionPane.WARNING_MESSAGE, 129 null, /* no special icon */ 130 specs, 131 specs[0], 132 ht("/Action/Upload#ChangesetFull") 133 ); 134 switch(ret) { 135 case 0: return MaxChangesetSizeExceededPolicy.AUTOMATICALLY_OPEN_NEW_CHANGESETS; 136 case 1: return MaxChangesetSizeExceededPolicy.FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG; 137 case 2: return MaxChangesetSizeExceededPolicy.ABORT; 138 case JOptionPane.CLOSED_OPTION: return MaxChangesetSizeExceededPolicy.ABORT; 139 } 140 // should not happen 141 return null; 142 } 143 144 protected void openNewChangeset() { 145 // make sure the current changeset is removed from the upload dialog. 146 // 147 ChangesetCache.getInstance().update(changeset); 148 Changeset newChangeSet = new Changeset(); 149 newChangeSet.setKeys(this.changeset.getKeys()); 150 this.changeset = newChangeSet; 151 } 152 153 protected boolean recoverFromChangesetFullException() { 154 if (toUpload.getSize() - processedPrimitives.size() == 0) { 155 strategy.setPolicy(MaxChangesetSizeExceededPolicy.ABORT); 156 return false; 157 } 158 if (strategy.getPolicy() == null || strategy.getPolicy().equals(MaxChangesetSizeExceededPolicy.ABORT)) { 159 MaxChangesetSizeExceededPolicy policy = askMaxChangesetSizeExceedsPolicy(); 160 strategy.setPolicy(policy); 161 } 162 switch(strategy.getPolicy()) { 163 case ABORT: 164 // don't continue - finish() will send the user back to map editing 165 // 166 return false; 167 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG: 168 // don't continue - finish() will send the user back to the upload dialog 169 // 170 return false; 171 case AUTOMATICALLY_OPEN_NEW_CHANGESETS: 172 // prepare the state of the task for a next iteration in uploading. 173 // 174 openNewChangeset(); 175 toUpload.removeProcessed(processedPrimitives); 176 return true; 177 } 178 // should not happen 179 return false; 180 } 181 182 /** 183 * Retries to recover the upload operation from an exception which was thrown because 184 * an uploaded primitive was already deleted on the server. 185 * 186 * @param e the exception throw by the API 187 * @param monitor a progress monitor 188 * @throws OsmTransferException thrown if we can't recover from the exception 189 */ 190 protected void recoverFromGoneOnServer(OsmApiPrimitiveGoneException e, ProgressMonitor monitor) throws OsmTransferException{ 191 if (!e.isKnownPrimitive()) throw e; 192 OsmPrimitive p = layer.data.getPrimitiveById(e.getPrimitiveId(), e.getPrimitiveType()); 193 if (p == null) throw e; 194 if (p.isDeleted()) { 195 // we tried to delete an already deleted primitive. 196 final String msg; 197 final String displayName = p.getDisplayName(DefaultNameFormatter.getInstance()); 198 if (p instanceof Node) { 199 msg = tr("Node ''{0}'' is already deleted. Skipping object in upload.", displayName); 200 } else if (p instanceof Way) { 201 msg = tr("Way ''{0}'' is already deleted. Skipping object in upload.", displayName); 202 } else if (p instanceof Relation) { 203 msg = tr("Relation ''{0}'' is already deleted. Skipping object in upload.", displayName); 204 } else { 205 msg = tr("Object ''{0}'' is already deleted. Skipping object in upload.", displayName); 206 } 207 monitor.appendLogMessage(msg); 208 Main.warn(msg); 209 processedPrimitives.addAll(writer.getProcessedPrimitives()); 210 processedPrimitives.add(p); 211 toUpload.removeProcessed(processedPrimitives); 212 return; 213 } 214 // exception was thrown because we tried to *update* an already deleted 215 // primitive. We can't resolve this automatically. Re-throw exception, 216 // a conflict is going to be created later. 217 throw e; 218 } 219 220 protected void cleanupAfterUpload() { 221 // we always clean up the data, even in case of errors. It's possible the data was 222 // partially uploaded. Better run on EDT. 223 // 224 Runnable r = new Runnable() { 225 @Override 226 public void run() { 227 layer.cleanupAfterUpload(processedPrimitives); 228 layer.onPostUploadToServer(); 229 ChangesetCache.getInstance().update(changeset); 230 } 231 }; 232 233 try { 234 SwingUtilities.invokeAndWait(r); 235 } catch(InterruptedException e) { 236 lastException = e; 237 } catch(InvocationTargetException e) { 238 lastException = new OsmTransferException(e.getCause()); 239 } 240 } 241 242 @Override protected void realRun() throws SAXException, IOException { 243 try { 244 uploadloop:while(true) { 245 try { 246 getProgressMonitor().subTask(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 e.printStackTrace(); 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 (Exception 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 if (uploadCanceled) 314 return; 315 316 // depending on the success of the upload operation and on the policy for 317 // multi changeset uploads this will sent the user back to the appropriate 318 // place in JOSM, either 319 // - to an error dialog 320 // - to the Upload Dialog 321 // - to map editing 322 GuiHelper.runInEDT(new Runnable() { 323 @Override 324 public void run() { 325 // if the changeset is still open after this upload we want it to 326 // be selected on the next upload 327 // 328 ChangesetCache.getInstance().update(changeset); 329 if (changeset != null && changeset.isOpen()) { 330 UploadDialog.getUploadDialog().setSelectedChangesetForNextUpload(changeset); 331 } 332 if (lastException == null) { 333 new Notification( 334 "<h3>" + tr("Upload successful!") + "</h3>") 335 .setIcon(ImageProvider.get("misc", "check_large")) 336 .show(); 337 return; 338 } 339 if (lastException instanceof ChangesetClosedException) { 340 ChangesetClosedException e = (ChangesetClosedException)lastException; 341 if (e.getSource().equals(ChangesetClosedException.Source.UPDATE_CHANGESET)) { 342 handleFailedUpload(lastException); 343 return; 344 } 345 if (strategy.getPolicy() == null) 346 /* do nothing if unknown policy */ 347 return; 348 if (e.getSource().equals(ChangesetClosedException.Source.UPLOAD_DATA)) { 349 switch(strategy.getPolicy()) { 350 case ABORT: 351 break; /* do nothing - we return to map editing */ 352 case AUTOMATICALLY_OPEN_NEW_CHANGESETS: 353 break; /* do nothing - we return to map editing */ 354 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG: 355 // return to the upload dialog 356 // 357 toUpload.removeProcessed(processedPrimitives); 358 UploadDialog.getUploadDialog().setUploadedPrimitives(toUpload); 359 UploadDialog.getUploadDialog().setVisible(true); 360 break; 361 } 362 } else { 363 handleFailedUpload(lastException); 364 } 365 } else { 366 handleFailedUpload(lastException); 367 } 368 } 369 }); 370 } 371 372 @Override protected void cancel() { 373 uploadCanceled = true; 374 synchronized(this) { 375 if (writer != null) { 376 writer.cancel(); 377 } 378 } 379 } 380}