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