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.I18n.tr; 006 007import java.awt.event.ActionEvent; 008import java.net.HttpURLConnection; 009import java.text.DateFormat; 010import java.util.Arrays; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.Date; 014import java.util.regex.Matcher; 015import java.util.regex.Pattern; 016 017import javax.swing.JOptionPane; 018 019import org.openstreetmap.josm.Main; 020import org.openstreetmap.josm.actions.DownloadReferrersAction; 021import org.openstreetmap.josm.actions.UpdateDataAction; 022import org.openstreetmap.josm.actions.UpdateSelectionAction; 023import org.openstreetmap.josm.data.osm.OsmPrimitive; 024import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 025import org.openstreetmap.josm.gui.ExceptionDialogUtil; 026import org.openstreetmap.josm.gui.HelpAwareOptionPane; 027import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 028import org.openstreetmap.josm.gui.MainApplication; 029import org.openstreetmap.josm.gui.PleaseWaitRunnable; 030import org.openstreetmap.josm.gui.layer.OsmDataLayer; 031import org.openstreetmap.josm.gui.progress.ProgressMonitor; 032import org.openstreetmap.josm.io.OsmApiException; 033import org.openstreetmap.josm.io.OsmApiInitializationException; 034import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException; 035import org.openstreetmap.josm.tools.ExceptionUtil; 036import org.openstreetmap.josm.tools.ImageProvider; 037import org.openstreetmap.josm.tools.Logging; 038import org.openstreetmap.josm.tools.Pair; 039import org.openstreetmap.josm.tools.date.DateUtils; 040 041/** 042 * Abstract base class for the task of uploading primitives via OSM API. 043 * 044 * Mainly handles conflicts and certain error situations. 045 */ 046public abstract class AbstractUploadTask extends PleaseWaitRunnable { 047 048 /** 049 * Constructs a new {@code AbstractUploadTask}. 050 * @param title message for the user 051 * @param ignoreException If true, exception will be silently ignored. If false then 052 * exception will be handled by showing a dialog. When this runnable is executed using executor framework 053 * then use false unless you read result of task (because exception will get lost if you don't) 054 */ 055 public AbstractUploadTask(String title, boolean ignoreException) { 056 super(title, ignoreException); 057 } 058 059 /** 060 * Constructs a new {@code AbstractUploadTask}. 061 * @param title message for the user 062 * @param progressMonitor progress monitor 063 * @param ignoreException If true, exception will be silently ignored. If false then 064 * exception will be handled by showing a dialog. When this runnable is executed using executor framework 065 * then use false unless you read result of task (because exception will get lost if you don't) 066 */ 067 public AbstractUploadTask(String title, ProgressMonitor progressMonitor, boolean ignoreException) { 068 super(title, progressMonitor, ignoreException); 069 } 070 071 /** 072 * Constructs a new {@code AbstractUploadTask}. 073 * @param title message for the user 074 */ 075 public AbstractUploadTask(String title) { 076 super(title); 077 } 078 079 /** 080 * Synchronizes the local state of an {@link OsmPrimitive} with its state on the 081 * server. The method uses an individual GET for the primitive. 082 * @param type the primitive type 083 * @param id the primitive ID 084 */ 085 protected void synchronizePrimitive(final OsmPrimitiveType type, final long id) { 086 // FIXME: should now about the layer this task is running for. might 087 // be different from the current edit layer 088 OsmDataLayer layer = MainApplication.getLayerManager().getEditLayer(); 089 if (layer == null) 090 throw new IllegalStateException(tr("Failed to update primitive with id {0} because current edit layer is null", id)); 091 OsmPrimitive p = layer.data.getPrimitiveById(id, type); 092 if (p == null) 093 throw new IllegalStateException( 094 tr("Failed to update primitive with id {0} because current edit layer does not include such a primitive", id)); 095 MainApplication.worker.execute(new UpdatePrimitivesTask(layer, Collections.singleton(p))); 096 } 097 098 /** 099 * Synchronizes the local state of the dataset with the state on the server. 100 * 101 * Reuses the functionality of {@link UpdateDataAction}. 102 * 103 * @see UpdateDataAction#actionPerformed(ActionEvent) 104 */ 105 protected void synchronizeDataSet() { 106 UpdateDataAction act = new UpdateDataAction(); 107 act.actionPerformed(new ActionEvent(this, 0, "")); 108 } 109 110 /** 111 * Handles the case that a conflict in a specific {@link OsmPrimitive} was detected while 112 * uploading 113 * 114 * @param primitiveType the type of the primitive, either <code>node</code>, <code>way</code> or 115 * <code>relation</code> 116 * @param id the id of the primitive 117 * @param serverVersion the version of the primitive on the server 118 * @param myVersion the version of the primitive in the local dataset 119 */ 120 protected void handleUploadConflictForKnownConflict(final OsmPrimitiveType primitiveType, final long id, String serverVersion, 121 String myVersion) { 122 String lbl; 123 switch(primitiveType) { 124 // CHECKSTYLE.OFF: SingleSpaceSeparator 125 case NODE: lbl = tr("Synchronize node {0} only", id); break; 126 case WAY: lbl = tr("Synchronize way {0} only", id); break; 127 case RELATION: lbl = tr("Synchronize relation {0} only", id); break; 128 // CHECKSTYLE.ON: SingleSpaceSeparator 129 default: throw new AssertionError(); 130 } 131 ButtonSpec[] spec = new ButtonSpec[] { 132 new ButtonSpec( 133 lbl, 134 ImageProvider.get("updatedata"), 135 null, 136 null 137 ), 138 new ButtonSpec( 139 tr("Synchronize entire dataset"), 140 ImageProvider.get("updatedata"), 141 null, 142 null 143 ), 144 new ButtonSpec( 145 tr("Cancel"), 146 ImageProvider.get("cancel"), 147 null, 148 null 149 ) 150 }; 151 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>" 152 + "of your nodes, ways, or relations.<br>" 153 + "The conflict is caused by the <strong>{0}</strong> with id <strong>{1}</strong>,<br>" 154 + "the server has version {2}, your version is {3}.<br>" 155 + "<br>" 156 + "Click <strong>{4}</strong> to synchronize the conflicting primitive only.<br>" 157 + "Click <strong>{5}</strong> to synchronize the entire local dataset with the server.<br>" 158 + "Click <strong>{6}</strong> to abort and continue editing.<br></html>", 159 tr(primitiveType.getAPIName()), id, serverVersion, myVersion, 160 spec[0].text, spec[1].text, spec[2].text 161 ); 162 int ret = HelpAwareOptionPane.showOptionDialog( 163 Main.parent, 164 msg, 165 tr("Conflicts detected"), 166 JOptionPane.ERROR_MESSAGE, 167 null, 168 spec, 169 spec[0], 170 "/Concepts/Conflict" 171 ); 172 switch(ret) { 173 case 0: synchronizePrimitive(primitiveType, id); break; 174 case 1: synchronizeDataSet(); break; 175 default: return; 176 } 177 } 178 179 /** 180 * Handles the case that a conflict was detected while uploading where we don't 181 * know what {@link OsmPrimitive} actually caused the conflict (for whatever reason) 182 * 183 */ 184 protected void handleUploadConflictForUnknownConflict() { 185 ButtonSpec[] spec = new ButtonSpec[] { 186 new ButtonSpec( 187 tr("Synchronize entire dataset"), 188 ImageProvider.get("updatedata"), 189 null, 190 null 191 ), 192 new ButtonSpec( 193 tr("Cancel"), 194 ImageProvider.get("cancel"), 195 null, 196 null 197 ) 198 }; 199 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>" 200 + "of your nodes, ways, or relations.<br>" 201 + "<br>" 202 + "Click <strong>{0}</strong> to synchronize the entire local dataset with the server.<br>" 203 + "Click <strong>{1}</strong> to abort and continue editing.<br></html>", 204 spec[0].text, spec[1].text 205 ); 206 int ret = HelpAwareOptionPane.showOptionDialog( 207 Main.parent, 208 msg, 209 tr("Conflicts detected"), 210 JOptionPane.ERROR_MESSAGE, 211 null, 212 spec, 213 spec[0], 214 ht("/Concepts/Conflict") 215 ); 216 if (ret == 0) { 217 synchronizeDataSet(); 218 } 219 } 220 221 /** 222 * Handles the case that a conflict was detected while uploading where we don't 223 * know what {@link OsmPrimitive} actually caused the conflict (for whatever reason) 224 * @param changesetId changeset ID 225 * @param d changeset date 226 */ 227 protected void handleUploadConflictForClosedChangeset(long changesetId, Date d) { 228 String msg = tr("<html>Uploading <strong>failed</strong> because you have been using<br>" 229 + "changeset {0} which was already closed at {1}.<br>" 230 + "Please upload again with a new or an existing open changeset.</html>", 231 changesetId, DateUtils.formatDateTime(d, DateFormat.SHORT, DateFormat.SHORT) 232 ); 233 JOptionPane.showMessageDialog( 234 Main.parent, 235 msg, 236 tr("Changeset closed"), 237 JOptionPane.ERROR_MESSAGE 238 ); 239 } 240 241 /** 242 * Handles the case where deleting a node failed because it is still in use in 243 * a non-deleted way on the server. 244 * @param e exception 245 * @param conflict conflict 246 */ 247 protected void handleUploadPreconditionFailedConflict(OsmApiException e, Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict) { 248 ButtonSpec[] options = new ButtonSpec[] { 249 new ButtonSpec( 250 tr("Prepare conflict resolution"), 251 ImageProvider.get("ok"), 252 tr("Click to download all referring objects for {0}", conflict.a), 253 null /* no specific help context */ 254 ), 255 new ButtonSpec( 256 tr("Cancel"), 257 ImageProvider.get("cancel"), 258 tr("Click to cancel and to resume editing the map"), 259 null /* no specific help context */ 260 ) 261 }; 262 String msg = ExceptionUtil.explainPreconditionFailed(e).replace("</html>", "<br><br>" + tr( 263 "Click <strong>{0}</strong> to load them now.<br>" 264 + "If necessary JOSM will create conflicts which you can resolve in the Conflict Resolution Dialog.", 265 options[0].text)) + "</html>"; 266 int ret = HelpAwareOptionPane.showOptionDialog( 267 Main.parent, 268 msg, 269 tr("Object still in use"), 270 JOptionPane.ERROR_MESSAGE, 271 null, 272 options, 273 options[0], 274 "/Action/Upload#NodeStillInUseInWay" 275 ); 276 if (ret == 0) { 277 DownloadReferrersAction.downloadReferrers(MainApplication.getLayerManager().getEditLayer(), Arrays.asList(conflict.a)); 278 } 279 } 280 281 /** 282 * handles an upload conflict, i.e. an error indicated by a HTTP return code 409. 283 * 284 * @param e the exception 285 */ 286 protected void handleUploadConflict(OsmApiException e) { 287 final String errorHeader = e.getErrorHeader(); 288 if (errorHeader != null) { 289 Pattern p = Pattern.compile("Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)"); 290 Matcher m = p.matcher(errorHeader); 291 if (m.matches()) { 292 handleUploadConflictForKnownConflict(OsmPrimitiveType.from(m.group(3)), Long.parseLong(m.group(4)), m.group(2), m.group(1)); 293 return; 294 } 295 p = Pattern.compile("The changeset (\\d+) was closed at (.*)"); 296 m = p.matcher(errorHeader); 297 if (m.matches()) { 298 handleUploadConflictForClosedChangeset(Long.parseLong(m.group(1)), DateUtils.fromString(m.group(2))); 299 return; 300 } 301 } 302 Logging.warn(tr("Error header \"{0}\" did not match with an expected pattern", errorHeader)); 303 handleUploadConflictForUnknownConflict(); 304 } 305 306 /** 307 * handles an precondition failed conflict, i.e. an error indicated by a HTTP return code 412. 308 * 309 * @param e the exception 310 */ 311 protected void handlePreconditionFailed(OsmApiException e) { 312 // in the worst case, ExceptionUtil.parsePreconditionFailed is executed trice - should not be too expensive 313 Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict = ExceptionUtil.parsePreconditionFailed(e.getErrorHeader()); 314 if (conflict != null) { 315 handleUploadPreconditionFailedConflict(e, conflict); 316 } else { 317 Logging.warn(tr("Error header \"{0}\" did not match with an expected pattern", e.getErrorHeader())); 318 ExceptionDialogUtil.explainPreconditionFailed(e); 319 } 320 } 321 322 /** 323 * Handles an error which is caused by a delete request for an already deleted 324 * {@link OsmPrimitive} on the server, i.e. a HTTP response code of 410. 325 * Note that an <strong>update</strong> on an already deleted object results 326 * in a 409, not a 410. 327 * 328 * @param e the exception 329 */ 330 protected void handleGone(OsmApiPrimitiveGoneException e) { 331 if (e.isKnownPrimitive()) { 332 UpdateSelectionAction.handlePrimitiveGoneException(e.getPrimitiveId(), e.getPrimitiveType()); 333 } else { 334 ExceptionDialogUtil.explainGoneForUnknownPrimitive(e); 335 } 336 } 337 338 /** 339 * error handler for any exception thrown during upload 340 * 341 * @param e the exception 342 */ 343 protected void handleFailedUpload(Exception e) { 344 // API initialization failed. Notify the user and return. 345 // 346 if (e instanceof OsmApiInitializationException) { 347 ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException) e); 348 return; 349 } 350 351 if (e instanceof OsmApiPrimitiveGoneException) { 352 handleGone((OsmApiPrimitiveGoneException) e); 353 return; 354 } 355 if (e instanceof OsmApiException) { 356 OsmApiException ex = (OsmApiException) e; 357 if (ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) { 358 // There was an upload conflict. Let the user decide whether and how to resolve it 359 handleUploadConflict(ex); 360 return; 361 } else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) { 362 // There was a precondition failed. Notify the user. 363 handlePreconditionFailed(ex); 364 return; 365 } else if (ex.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { 366 // Tried to update or delete a primitive which never existed on the server? 367 ExceptionDialogUtil.explainNotFound(ex); 368 return; 369 } 370 } 371 372 ExceptionDialogUtil.explainException(e); 373 } 374}