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