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