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