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