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