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 }