// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.gui.io; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.event.ActionEvent; import java.net.HttpURLConnection; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.JOptionPane; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.actions.DownloadReferrersAction; import org.openstreetmap.josm.actions.UpdateDataAction; import org.openstreetmap.josm.actions.UpdateSelectionAction; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmPrimitiveType; import org.openstreetmap.josm.gui.ExceptionDialogUtil; import org.openstreetmap.josm.gui.HelpAwareOptionPane; import org.openstreetmap.josm.gui.PleaseWaitRunnable; import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; import org.openstreetmap.josm.gui.layer.OsmDataLayer; import org.openstreetmap.josm.gui.progress.ProgressMonitor; import org.openstreetmap.josm.io.OsmApiException; import org.openstreetmap.josm.io.OsmApiInitializationException; import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException; import org.openstreetmap.josm.tools.DateUtils; import org.openstreetmap.josm.tools.ImageProvider; public abstract class AbstractUploadTask extends PleaseWaitRunnable { private static final Logger logger = Logger.getLogger(AbstractUploadTask.class.getName()); public AbstractUploadTask(String title, boolean ignoreException) { super(title, ignoreException); } public AbstractUploadTask(String title, ProgressMonitor progressMonitor, boolean ignoreException) { super(title, progressMonitor, ignoreException); } public AbstractUploadTask(String title) { super(title); } /** * Synchronizes the local state of an {@see OsmPrimitive} with its state on the * server. The method uses an individual GET for the primitive. * * @param id the primitive ID */ protected void synchronizePrimitive(final OsmPrimitiveType type, final long id) { // FIXME: should now about the layer this task is running for. might // be different from the current edit layer OsmDataLayer layer = Main.main.getEditLayer(); if (layer == null) throw new IllegalStateException(tr("Failed to update primitive with id {0} because current edit layer is null", id)); OsmPrimitive p = layer.data.getPrimitiveById(id, type); if (p == null) throw new IllegalStateException(tr("Failed to update primitive with id {0} because current edit layer does not include such a primitive", id)); Main.worker.execute(new UpdatePrimitivesTask(layer, Collections.singleton(p))); } /** * Synchronizes the local state of the dataset with the state on the server. * * Reuses the functionality of {@see UpdateDataAction}. * * @see UpdateDataAction#actionPerformed(ActionEvent) */ protected void synchronizeDataSet() { UpdateDataAction act = new UpdateDataAction(); act.actionPerformed(new ActionEvent(this,0,"")); } /** * Handles the case that a conflict in a specific {@see OsmPrimitive} was detected while * uploading * * @param primitiveType the type of the primitive, either <code>node</code>, <code>way</code> or * <code>relation</code> * @param id the id of the primitive * @param serverVersion the version of the primitive on the server * @param myVersion the version of the primitive in the local dataset */ protected void handleUploadConflictForKnownConflict(final OsmPrimitiveType primitiveType, final long id, String serverVersion, String myVersion) { String lbl = ""; switch(primitiveType) { case NODE: lbl = tr("Synchronize node {0} only", id); break; case WAY: lbl = tr("Synchronize way {0} only", id); break; case RELATION: lbl = tr("Synchronize relation {0} only", id); break; } ButtonSpec[] spec = new ButtonSpec[] { new ButtonSpec( lbl, ImageProvider.get("updatedata"), null, null ), new ButtonSpec( tr("Synchronize entire dataset"), ImageProvider.get("updatedata"), null, null ), new ButtonSpec( tr("Cancel"), ImageProvider.get("cancel"), null, null ) }; String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>" + "of your nodes, ways, or relations.<br>" + "The conflict is caused by the <strong>{0}</strong> with id <strong>{1}</strong>,<br>" + "the server has version {2}, your version is {3}.<br>" + "<br>" + "Click <strong>{4}</strong> to synchronize the conflicting primitive only.<br>" + "Click <strong>{5}</strong> to synchronize the entire local dataset with the server.<br>" + "Click <strong>{6}</strong> to abort and continue editing.<br></html>", tr(primitiveType.getAPIName()), id, serverVersion, myVersion, spec[0].text, spec[1].text, spec[2].text ); int ret = HelpAwareOptionPane.showOptionDialog( Main.parent, msg, tr("Conflicts detected"), JOptionPane.ERROR_MESSAGE, null, spec, spec[0], "/Concepts/Conflict" ); switch(ret) { case 0: synchronizePrimitive(primitiveType, id); break; case 1: synchronizeDataSet(); break; default: return; } } /** * Handles the case that a conflict was detected while uploading where we don't * know what {@see OsmPrimitive} actually caused the conflict (for whatever reason) * */ protected void handleUploadConflictForUnknownConflict() { ButtonSpec[] spec = new ButtonSpec[] { new ButtonSpec( tr("Synchronize entire dataset"), ImageProvider.get("updatedata"), null, null ), new ButtonSpec( tr("Cancel"), ImageProvider.get("cancel"), null, null ) }; String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>" + "of your nodes, ways, or relations.<br>" + "<br>" + "Click <strong>{0}</strong> to synchronize the entire local dataset with the server.<br>" + "Click <strong>{1}</strong> to abort and continue editing.<br></html>", spec[0].text, spec[1].text ); int ret = HelpAwareOptionPane.showOptionDialog( Main.parent, msg, tr("Conflicts detected"), JOptionPane.ERROR_MESSAGE, null, spec, spec[0], ht("Concepts/Conflict") ); if (ret == 0) { synchronizeDataSet(); } } /** * Handles the case that a conflict was detected while uploading where we don't * know what {@see OsmPrimitive} actually caused the conflict (for whatever reason) * */ protected void handleUploadConflictForClosedChangeset(long changesetId, Date d) { String msg = tr("<html>Uploading <strong>failed</strong> because you have been using<br>" + "changeset {0} which was already closed at {1}.<br>" + "Please upload again with a new or an existing open changeset.</html>", changesetId, new SimpleDateFormat().format(d) ); JOptionPane.showMessageDialog( Main.parent, msg, tr("Changeset closed"), JOptionPane.ERROR_MESSAGE ); } /** * Handles the case where deleting a node failed because it is still in use in * a non-deleted way on the server. */ protected void handleUploadConflictForNodeStillInUse(long nodeId, long wayId) { ButtonSpec[] options = new ButtonSpec[] { new ButtonSpec( tr("Prepare conflict resolution"), ImageProvider.get("ok"), tr("Click to download all parent ways for node {0}", nodeId), null /* no specific help context */ ), new ButtonSpec( tr("Cancel"), ImageProvider.get("cancel"), tr("Click to cancel and to resume editing the map", nodeId), null /* no specific help context */ ) }; String msg = tr("<html>Uploading <strong>failed</strong> because you tried " + "to delete node {0} which is still in use in way {1}.<br><br>" + "Click <strong>{2}</strong> to download all parent ways of node {0}.<br>" + "If necessary JOSM will create conflicts which you can resolve in the Conflict Resolution Dialog." + "</html>", nodeId, wayId, options[0].text ); int ret = HelpAwareOptionPane.showOptionDialog( Main.parent, msg, tr("Node still in use"), JOptionPane.ERROR_MESSAGE, null, options, options[0], "/Action/Upload#NodeStillInUseInWay" ); if (ret != 0) return; DownloadReferrersAction.downloadReferrers(Main.map.mapView.getEditLayer(), nodeId, OsmPrimitiveType.NODE); } /** * handles an upload conflict, i.e. an error indicated by a HTTP return code 409. * * @param e the exception */ protected void handleUploadConflict(OsmApiException e) { String pattern = "Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)"; Pattern p = Pattern.compile(pattern); Matcher m = p.matcher(e.getErrorHeader()); if (m.matches()) { handleUploadConflictForKnownConflict(OsmPrimitiveType.from(m.group(3)), Long.parseLong(m.group(4)), m.group(2),m.group(1)); return; } pattern ="The changeset (\\d+) was closed at (.*)"; p = Pattern.compile(pattern); m = p.matcher(e.getErrorHeader()); if (m.matches()) { handleUploadConflictForClosedChangeset(Long.parseLong(m.group(1)), DateUtils.fromString(m.group(2))); return; } pattern = "Node (\\d+) is still used by way (\\d+)."; p = Pattern.compile(pattern); m = p.matcher(e.getErrorHeader()); if (m.matches()) { handleUploadConflictForNodeStillInUse(Long.parseLong(m.group(1)), Long.parseLong(m.group(2))); return; } logger.warning(tr("Warning: error header \"{0}\" did not match with an expected pattern", e.getErrorHeader())); handleUploadConflictForUnknownConflict(); } /** * handles an precondition failed conflict, i.e. an error indicated by a HTTP return code 412. * * @param e the exception */ protected void handlePreconditionFailed(OsmApiException e) { String pattern = "Precondition failed: Node (\\d+) is still used by way (\\d+)."; Pattern p = Pattern.compile(pattern); Matcher m = p.matcher(e.getErrorHeader()); if (m.matches()) { handleUploadConflictForNodeStillInUse(Long.parseLong(m.group(1)), Long.parseLong(m.group(2))); return; } logger.warning(tr("Warning: error header \"{0}\" did not match with an expected pattern", e.getErrorHeader())); ExceptionDialogUtil.explainPreconditionFailed(e); } /** * Handles an error which is caused by a delete request for an already deleted * {@see OsmPrimitive} on the server, i.e. a HTTP response code of 410. * Note that an <strong>update</strong> on an already deleted object results * in a 409, not a 410. * * @param e the exception */ protected void handleGone(OsmApiPrimitiveGoneException e) { if (e.isKnownPrimitive()) { new UpdateSelectionAction().handlePrimitiveGoneException(e.getPrimitiveId(),e.getPrimitiveType()); } else { ExceptionDialogUtil.explainGoneForUnknownPrimitive(e); } } /** * error handler for any exception thrown during upload * * @param e the exception */ protected void handleFailedUpload(Exception e) { // API initialization failed. Notify the user and return. // if (e instanceof OsmApiInitializationException) { ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException)e); return; } if (e instanceof OsmApiPrimitiveGoneException) { handleGone((OsmApiPrimitiveGoneException)e); return; } if (e instanceof OsmApiException) { OsmApiException ex = (OsmApiException)e; // There was an upload conflict. Let the user decide whether // and how to resolve it // if(ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) { handleUploadConflict(ex); return; } // There was a precondition failed. Notify the user. // else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) { handlePreconditionFailed(ex); return; } // Tried to update or delete a primitive which never existed on // the server? // else if (ex.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { ExceptionDialogUtil.explainNotFound(ex); return; } } ExceptionDialogUtil.explainException(e); } }