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 new ImageProvider("updatedata"), 135 null, null), 136 new ButtonSpec( 137 tr("Synchronize entire dataset"), 138 new ImageProvider("updatedata"), 139 null, null), 140 new ButtonSpec( 141 tr("Cancel"), 142 new ImageProvider("cancel"), 143 null, null) 144 }; 145 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>" 146 + "of your nodes, ways, or relations.<br>" 147 + "The conflict is caused by the <strong>{0}</strong> with id <strong>{1}</strong>,<br>" 148 + "the server has version {2}, your version is {3}.<br>" 149 + "<br>" 150 + "Click <strong>{4}</strong> to synchronize the conflicting primitive only.<br>" 151 + "Click <strong>{5}</strong> to synchronize the entire local dataset with the server.<br>" 152 + "Click <strong>{6}</strong> to abort and continue editing.<br></html>", 153 tr(primitiveType.getAPIName()), id, serverVersion, myVersion, 154 spec[0].text, spec[1].text, spec[2].text 155 ); 156 int ret = HelpAwareOptionPane.showOptionDialog( 157 Main.parent, 158 msg, 159 tr("Conflicts detected"), 160 JOptionPane.ERROR_MESSAGE, 161 null, 162 spec, 163 spec[0], 164 "/Concepts/Conflict" 165 ); 166 switch(ret) { 167 case 0: synchronizePrimitive(primitiveType, id); break; 168 case 1: synchronizeDataSet(); break; 169 default: return; 170 } 171 } 172 173 /** 174 * Handles the case that a conflict was detected while uploading where we don't 175 * know what {@link OsmPrimitive} actually caused the conflict (for whatever reason) 176 * 177 */ 178 protected void handleUploadConflictForUnknownConflict() { 179 ButtonSpec[] spec = new ButtonSpec[] { 180 new ButtonSpec( 181 tr("Synchronize entire dataset"), 182 new ImageProvider("updatedata"), 183 null, null), 184 new ButtonSpec( 185 tr("Cancel"), 186 new ImageProvider("cancel"), 187 null, null) 188 }; 189 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>" 190 + "of your nodes, ways, or relations.<br>" 191 + "<br>" 192 + "Click <strong>{0}</strong> to synchronize the entire local dataset with the server.<br>" 193 + "Click <strong>{1}</strong> to abort and continue editing.<br></html>", 194 spec[0].text, spec[1].text 195 ); 196 int ret = HelpAwareOptionPane.showOptionDialog( 197 Main.parent, 198 msg, 199 tr("Conflicts detected"), 200 JOptionPane.ERROR_MESSAGE, 201 null, 202 spec, 203 spec[0], 204 ht("/Concepts/Conflict") 205 ); 206 if (ret == 0) { 207 synchronizeDataSet(); 208 } 209 } 210 211 /** 212 * Handles the case that a conflict was detected while uploading where we don't 213 * know what {@link OsmPrimitive} actually caused the conflict (for whatever reason) 214 * @param changesetId changeset ID 215 * @param d changeset date 216 */ 217 protected void handleUploadConflictForClosedChangeset(long changesetId, Date d) { 218 String msg = tr("<html>Uploading <strong>failed</strong> because you have been using<br>" 219 + "changeset {0} which was already closed at {1}.<br>" 220 + "Please upload again with a new or an existing open changeset.</html>", 221 changesetId, DateUtils.formatDateTime(d, DateFormat.SHORT, DateFormat.SHORT) 222 ); 223 JOptionPane.showMessageDialog( 224 Main.parent, 225 msg, 226 tr("Changeset closed"), 227 JOptionPane.ERROR_MESSAGE 228 ); 229 } 230 231 /** 232 * Handles the case where deleting a node failed because it is still in use in 233 * a non-deleted way on the server. 234 * @param e exception 235 * @param conflict conflict 236 */ 237 protected void handleUploadPreconditionFailedConflict(OsmApiException e, Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict) { 238 ButtonSpec[] options = new ButtonSpec[] { 239 new ButtonSpec( 240 tr("Prepare conflict resolution"), 241 new ImageProvider("ok"), 242 tr("Click to download all referring objects for {0}", conflict.a), 243 null /* no specific help context */ 244 ), 245 new ButtonSpec( 246 tr("Cancel"), 247 new ImageProvider("cancel"), 248 tr("Click to cancel and to resume editing the map"), 249 null /* no specific help context */ 250 ) 251 }; 252 String msg = ExceptionUtil.explainPreconditionFailed(e).replace("</html>", "<br><br>" + tr( 253 "Click <strong>{0}</strong> to load them now.<br>" 254 + "If necessary JOSM will create conflicts which you can resolve in the Conflict Resolution Dialog.", 255 options[0].text)) + "</html>"; 256 int ret = HelpAwareOptionPane.showOptionDialog( 257 Main.parent, 258 msg, 259 tr("Object still in use"), 260 JOptionPane.ERROR_MESSAGE, 261 null, 262 options, 263 options[0], 264 "/Action/Upload#NodeStillInUseInWay" 265 ); 266 if (ret == 0) { 267 DownloadReferrersAction.downloadReferrers(MainApplication.getLayerManager().getEditLayer(), Arrays.asList(conflict.a)); 268 } 269 } 270 271 /** 272 * handles an upload conflict, i.e. an error indicated by a HTTP return code 409. 273 * 274 * @param e the exception 275 */ 276 protected void handleUploadConflict(OsmApiException e) { 277 final String errorHeader = e.getErrorHeader(); 278 if (errorHeader != null) { 279 Pattern p = Pattern.compile("Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)"); 280 Matcher m = p.matcher(errorHeader); 281 if (m.matches()) { 282 handleUploadConflictForKnownConflict(OsmPrimitiveType.from(m.group(3)), Long.parseLong(m.group(4)), m.group(2), m.group(1)); 283 return; 284 } 285 p = Pattern.compile("The changeset (\\d+) was closed at (.*)"); 286 m = p.matcher(errorHeader); 287 if (m.matches()) { 288 handleUploadConflictForClosedChangeset(Long.parseLong(m.group(1)), DateUtils.fromString(m.group(2))); 289 return; 290 } 291 } 292 Logging.warn(tr("Error header \"{0}\" did not match with an expected pattern", errorHeader)); 293 handleUploadConflictForUnknownConflict(); 294 } 295 296 /** 297 * handles an precondition failed conflict, i.e. an error indicated by a HTTP return code 412. 298 * 299 * @param e the exception 300 */ 301 protected void handlePreconditionFailed(OsmApiException e) { 302 // in the worst case, ExceptionUtil.parsePreconditionFailed is executed trice - should not be too expensive 303 Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict = ExceptionUtil.parsePreconditionFailed(e.getErrorHeader()); 304 if (conflict != null) { 305 handleUploadPreconditionFailedConflict(e, conflict); 306 } else { 307 Logging.warn(tr("Error header \"{0}\" did not match with an expected pattern", e.getErrorHeader())); 308 ExceptionDialogUtil.explainPreconditionFailed(e); 309 } 310 } 311 312 /** 313 * Handles an error which is caused by a delete request for an already deleted 314 * {@link OsmPrimitive} on the server, i.e. a HTTP response code of 410. 315 * Note that an <strong>update</strong> on an already deleted object results 316 * in a 409, not a 410. 317 * 318 * @param e the exception 319 */ 320 protected void handleGone(OsmApiPrimitiveGoneException e) { 321 if (e.isKnownPrimitive()) { 322 UpdateSelectionAction.handlePrimitiveGoneException(e.getPrimitiveId(), e.getPrimitiveType()); 323 } else { 324 ExceptionDialogUtil.explainGoneForUnknownPrimitive(e); 325 } 326 } 327 328 /** 329 * error handler for any exception thrown during upload 330 * 331 * @param e the exception 332 */ 333 protected void handleFailedUpload(Exception e) { 334 // API initialization failed. Notify the user and return. 335 // 336 if (e instanceof OsmApiInitializationException) { 337 ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException) e); 338 return; 339 } 340 341 if (e instanceof OsmApiPrimitiveGoneException) { 342 handleGone((OsmApiPrimitiveGoneException) e); 343 return; 344 } 345 if (e instanceof OsmApiException) { 346 OsmApiException ex = (OsmApiException) e; 347 if (ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) { 348 // There was an upload conflict. Let the user decide whether and how to resolve it 349 handleUploadConflict(ex); 350 return; 351 } else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) { 352 // There was a precondition failed. Notify the user. 353 handlePreconditionFailed(ex); 354 return; 355 } else if (ex.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { 356 // Tried to update or delete a primitive which never existed on the server? 357 ExceptionDialogUtil.explainNotFound(ex); 358 return; 359 } 360 } 361 362 ExceptionDialogUtil.explainException(e); 363 } 364}