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.DefaultNameFormatter; 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.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.gui.widgets.HtmlPanel; 033import org.openstreetmap.josm.io.ChangesetClosedException; 034import org.openstreetmap.josm.io.MaxChangesetSizeExceededPolicy; 035import org.openstreetmap.josm.io.MessageNotifier; 036import org.openstreetmap.josm.io.OsmApi; 037import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException; 038import org.openstreetmap.josm.io.OsmServerWriter; 039import org.openstreetmap.josm.io.OsmTransferCanceledException; 040import org.openstreetmap.josm.io.OsmTransferException; 041import org.openstreetmap.josm.io.UploadStrategySpecification; 042import org.openstreetmap.josm.tools.ImageProvider; 043import org.openstreetmap.josm.tools.Logging; 044 045/** 046 * The task for uploading a collection of primitives. 047 * @since 2599 048 */ 049public class UploadPrimitivesTask extends AbstractUploadTask { 050 private boolean uploadCanceled; 051 private Exception lastException; 052 private final APIDataSet toUpload; 053 private OsmServerWriter writer; 054 private final OsmDataLayer layer; 055 private Changeset changeset; 056 private final Set<IPrimitive> processedPrimitives; 057 private final UploadStrategySpecification strategy; 058 059 /** 060 * Creates the task 061 * 062 * @param strategy the upload strategy. Must not be null. 063 * @param layer the OSM data layer for which data is uploaded. Must not be null. 064 * @param toUpload the collection of primitives to upload. Set to the empty collection if null. 065 * @param changeset the changeset to use for uploading. Must not be null. changeset.getId() 066 * can be 0 in which case the upload task creates a new changeset 067 * @throws IllegalArgumentException if layer is null 068 * @throws IllegalArgumentException if toUpload is null 069 * @throws IllegalArgumentException if strategy is null 070 * @throws IllegalArgumentException if changeset is null 071 */ 072 public UploadPrimitivesTask(UploadStrategySpecification strategy, OsmDataLayer layer, APIDataSet toUpload, Changeset changeset) { 073 super(tr("Uploading data for layer ''{0}''", layer.getName()), false /* don't ignore exceptions */); 074 ensureParameterNotNull(layer, "layer"); 075 ensureParameterNotNull(strategy, "strategy"); 076 ensureParameterNotNull(changeset, "changeset"); 077 this.toUpload = toUpload; 078 this.layer = layer; 079 this.changeset = changeset; 080 this.strategy = strategy; 081 this.processedPrimitives = new HashSet<>(); 082 } 083 084 protected MaxChangesetSizeExceededPolicy askMaxChangesetSizeExceedsPolicy() { 085 ButtonSpec[] specs = new ButtonSpec[] { 086 new ButtonSpec( 087 tr("Continue uploading"), 088 ImageProvider.get("upload"), 089 tr("Click to continue uploading to additional new changesets"), 090 null /* no specific help text */ 091 ), 092 new ButtonSpec( 093 tr("Go back to Upload Dialog"), 094 ImageProvider.get("dialogs", "uploadproperties"), 095 tr("Click to return to the Upload Dialog"), 096 null /* no specific help text */ 097 ), 098 new ButtonSpec( 099 tr("Abort"), 100 ImageProvider.get("cancel"), 101 tr("Click to abort uploading"), 102 null /* no specific help text */ 103 ) 104 }; 105 int numObjectsToUploadLeft = toUpload.getSize() - processedPrimitives.size(); 106 String msg1 = tr("The server reported that the current changeset was closed.<br>" 107 + "This is most likely because the changesets size exceeded the max. size<br>" 108 + "of {0} objects on the server ''{1}''.", 109 OsmApi.getOsmApi().getCapabilities().getMaxChangesetSize(), 110 OsmApi.getOsmApi().getBaseUrl() 111 ); 112 String msg2 = trn( 113 "There is {0} object left to upload.", 114 "There are {0} objects left to upload.", 115 numObjectsToUploadLeft, 116 numObjectsToUploadLeft 117 ); 118 String msg3 = tr( 119 "Click ''<strong>{0}</strong>'' to continue uploading to additional new changesets.<br>" 120 + "Click ''<strong>{1}</strong>'' to return to the upload dialog.<br>" 121 + "Click ''<strong>{2}</strong>'' to abort uploading and return to map editing.<br>", 122 specs[0].text, 123 specs[1].text, 124 specs[2].text 125 ); 126 String msg = "<html>" + msg1 + "<br><br>" + msg2 +"<br><br>" + msg3 + "</html>"; 127 int ret = HelpAwareOptionPane.showOptionDialog( 128 Main.parent, 129 msg, 130 tr("Changeset is full"), 131 JOptionPane.WARNING_MESSAGE, 132 null, /* no special icon */ 133 specs, 134 specs[0], 135 ht("/Action/Upload#ChangesetFull") 136 ); 137 switch(ret) { 138 case 0: return MaxChangesetSizeExceededPolicy.AUTOMATICALLY_OPEN_NEW_CHANGESETS; 139 case 1: return MaxChangesetSizeExceededPolicy.FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG; 140 case 2: 141 case JOptionPane.CLOSED_OPTION: 142 default: return MaxChangesetSizeExceededPolicy.ABORT; 143 } 144 } 145 146 /** 147 * Opens a new changeset. 148 */ 149 protected void openNewChangeset() { 150 // make sure the current changeset is removed from the upload dialog. 151 ChangesetCache.getInstance().update(changeset); 152 Changeset newChangeSet = new Changeset(); 153 newChangeSet.setKeys(this.changeset.getKeys()); 154 this.changeset = newChangeSet; 155 } 156 157 protected boolean recoverFromChangesetFullException() throws OsmTransferException { 158 if (toUpload.getSize() - processedPrimitives.size() == 0) { 159 strategy.setPolicy(MaxChangesetSizeExceededPolicy.ABORT); 160 return false; 161 } 162 if (strategy.getPolicy() == null || strategy.getPolicy().equals(MaxChangesetSizeExceededPolicy.ABORT)) { 163 strategy.setPolicy(askMaxChangesetSizeExceedsPolicy()); 164 } 165 switch(strategy.getPolicy()) { 166 case AUTOMATICALLY_OPEN_NEW_CHANGESETS: 167 // prepare the state of the task for a next iteration in uploading. 168 closeChangesetIfRequired(); 169 openNewChangeset(); 170 toUpload.removeProcessed(processedPrimitives); 171 return true; 172 case ABORT: 173 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG: 174 default: 175 // don't continue - finish() will send the user back to map editing or upload dialog 176 return false; 177 } 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 Logging.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 Runnable r = () -> { 222 boolean readOnly = layer.isLocked(); 223 if (readOnly) { 224 layer.unlock(); 225 } 226 try { 227 layer.cleanupAfterUpload(processedPrimitives); 228 layer.onPostUploadToServer(); 229 ChangesetCache.getInstance().update(changeset); 230 } finally { 231 if (readOnly) { 232 layer.lock(); 233 } 234 } 235 }; 236 237 try { 238 SwingUtilities.invokeAndWait(r); 239 } catch (InterruptedException e) { 240 Logging.trace(e); 241 lastException = e; 242 Thread.currentThread().interrupt(); 243 } catch (InvocationTargetException e) { 244 Logging.trace(e); 245 lastException = new OsmTransferException(e.getCause()); 246 } 247 } 248 249 @Override 250 protected void realRun() { 251 try { 252 MessageNotifier.stop(); 253 uploadloop: while (true) { 254 try { 255 getProgressMonitor().subTask( 256 trn("Uploading {0} object...", "Uploading {0} objects...", toUpload.getSize(), toUpload.getSize())); 257 synchronized (this) { 258 writer = new OsmServerWriter(); 259 } 260 writer.uploadOsm(strategy, toUpload.getPrimitives(), changeset, getProgressMonitor().createSubTaskMonitor(1, false)); 261 262 // if we get here we've successfully uploaded the data. Exit the loop. 263 break; 264 } catch (OsmTransferCanceledException e) { 265 Logging.error(e); 266 uploadCanceled = true; 267 break uploadloop; 268 } catch (OsmApiPrimitiveGoneException e) { 269 // try to recover from 410 Gone 270 recoverFromGoneOnServer(e, getProgressMonitor()); 271 } catch (ChangesetClosedException e) { 272 if (writer != null) { 273 processedPrimitives.addAll(writer.getProcessedPrimitives()); // OsmPrimitive in => OsmPrimitive out 274 } 275 switch(e.getSource()) { 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 if (recoverFromChangesetFullException()) { 281 continue; 282 } 283 lastException = e; 284 break uploadloop; 285 case UNSPECIFIED: 286 case UPDATE_CHANGESET: 287 default: 288 // The changeset was closed when we tried to update it. Probably, our 289 // local list of open changesets got out of sync with the server state. 290 // The user will have to select another open changeset. 291 // Rethrow exception - this will be handled later. 292 changeset.setOpen(false); 293 throw e; 294 } 295 } finally { 296 if (writer != null) { 297 processedPrimitives.addAll(writer.getProcessedPrimitives()); 298 } 299 synchronized (this) { 300 writer = null; 301 } 302 } 303 } 304 // if required close the changeset 305 closeChangesetIfRequired(); 306 } catch (OsmTransferException e) { 307 if (uploadCanceled) { 308 Logging.info(tr("Ignoring caught exception because upload is canceled. Exception is: {0}", e.toString())); 309 } else { 310 lastException = e; 311 } 312 } finally { 313 if (MessageNotifier.PROP_NOTIFIER_ENABLED.get()) { 314 MessageNotifier.start(); 315 } 316 } 317 if (uploadCanceled && processedPrimitives.isEmpty()) return; 318 cleanupAfterUpload(); 319 } 320 321 private void closeChangesetIfRequired() throws OsmTransferException { 322 if (strategy.isCloseChangesetAfterUpload() && changeset != null && !changeset.isNew() && changeset.isOpen()) { 323 OsmApi.getOsmApi().closeChangeset(changeset, progressMonitor.createSubTaskMonitor(0, false)); 324 } 325 } 326 327 @Override protected void finish() { 328 329 // depending on the success of the upload operation and on the policy for 330 // multi changeset uploads this will sent the user back to the appropriate 331 // place in JOSM, either 332 // - to an error dialog 333 // - to the Upload Dialog 334 // - to map editing 335 GuiHelper.runInEDT(() -> { 336 // if the changeset is still open after this upload we want it to be selected on the next upload 337 ChangesetCache.getInstance().update(changeset); 338 if (changeset != null && changeset.isOpen()) { 339 UploadDialog.getUploadDialog().setSelectedChangesetForNextUpload(changeset); 340 } 341 if (uploadCanceled) return; 342 if (lastException == null) { 343 HtmlPanel panel = new HtmlPanel( 344 "<h3><a href=\"" + Main.getBaseBrowseUrl() + "/changeset/" + changeset.getId() + "\">" 345 + tr("Upload successful!") + "</a></h3>"); 346 panel.enableClickableHyperlinks(); 347 panel.setOpaque(false); 348 new Notification() 349 .setContent(panel) 350 .setIcon(ImageProvider.get("misc", "check_large")) 351 .show(); 352 return; 353 } 354 if (lastException instanceof ChangesetClosedException) { 355 ChangesetClosedException e = (ChangesetClosedException) lastException; 356 if (e.getSource().equals(ChangesetClosedException.Source.UPDATE_CHANGESET)) { 357 handleFailedUpload(lastException); 358 return; 359 } 360 if (strategy.getPolicy() == null) 361 /* do nothing if unknown policy */ 362 return; 363 if (e.getSource().equals(ChangesetClosedException.Source.UPLOAD_DATA)) { 364 switch(strategy.getPolicy()) { 365 case ABORT: 366 break; /* do nothing - we return to map editing */ 367 case AUTOMATICALLY_OPEN_NEW_CHANGESETS: 368 break; /* do nothing - we return to map editing */ 369 case FILL_ONE_CHANGESET_AND_RETURN_TO_UPLOAD_DIALOG: 370 // return to the upload dialog 371 // 372 toUpload.removeProcessed(processedPrimitives); 373 UploadDialog.getUploadDialog().setUploadedPrimitives(toUpload); 374 UploadDialog.getUploadDialog().setVisible(true); 375 break; 376 } 377 } else { 378 handleFailedUpload(lastException); 379 } 380 } else { 381 handleFailedUpload(lastException); 382 } 383 }); 384 } 385 386 @Override protected void cancel() { 387 uploadCanceled = true; 388 synchronized (this) { 389 if (writer != null) { 390 writer.cancel(); 391 } 392 } 393 } 394}