// 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.DateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
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.HelpAwareOptionPane.ButtonSpec;
import org.openstreetmap.josm.gui.PleaseWaitRunnable;
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.ExceptionUtil;
import org.openstreetmap.josm.tools.ImageProvider;
import org.openstreetmap.josm.tools.Pair;
import org.openstreetmap.josm.tools.date.DateUtils;
public abstract class AbstractUploadTask extends PleaseWaitRunnable {
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 {@link 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 {@link 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 {@link 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 {@link 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 {@link 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, DateUtils.formatDateTime(d, DateFormat.SHORT, DateFormat.SHORT)
);
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 handleUploadPreconditionFailedConflict(OsmApiException e, Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict) {
ButtonSpec[] options = new ButtonSpec[] {
new ButtonSpec(
tr("Prepare conflict resolution"),
ImageProvider.get("ok"),
tr("Click to download all referring objects for {0}", conflict.a),
null /* no specific help context */
),
new ButtonSpec(
tr("Cancel"),
ImageProvider.get("cancel"),
tr("Click to cancel and to resume editing the map"),
null /* no specific help context */
)
};
String msg = ExceptionUtil.explainPreconditionFailed(e).replace("</html>", "<br><br>" + tr(
"Click <strong>{0}</strong> to load them now.<br>"
+ "If necessary JOSM will create conflicts which you can resolve in the Conflict Resolution Dialog.",
options[0].text)) + "</html>";
int ret = HelpAwareOptionPane.showOptionDialog(
Main.parent,
msg,
tr("Object still in use"),
JOptionPane.ERROR_MESSAGE,
null,
options,
options[0],
"/Action/Upload#NodeStillInUseInWay"
);
if (ret == 0) {
DownloadReferrersAction.downloadReferrers(Main.main.getEditLayer(), Arrays.asList(conflict.a));
}
}
/**
* handles an upload conflict, i.e. an error indicated by a HTTP return code 409.
*
* @param e the exception
*/
protected void handleUploadConflict(OsmApiException e) {
final String errorHeader = e.getErrorHeader();
if (errorHeader != null) {
Pattern p = Pattern.compile("Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)");
Matcher m = p.matcher(errorHeader);
if (m.matches()) {
handleUploadConflictForKnownConflict(OsmPrimitiveType.from(m.group(3)), Long.parseLong(m.group(4)), m.group(2),m.group(1));
return;
}
p = Pattern.compile("The changeset (\\d+) was closed at (.*)");
m = p.matcher(errorHeader);
if (m.matches()) {
handleUploadConflictForClosedChangeset(Long.parseLong(m.group(1)), DateUtils.fromString(m.group(2)));
return;
}
}
Main.warn(tr("Error header \"{0}\" did not match with an expected pattern", errorHeader));
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) {
// in the worst case, ExceptionUtil.parsePreconditionFailed is executed trice - should not be too expensive
Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict = ExceptionUtil.parsePreconditionFailed(e.getErrorHeader());
if (conflict != null) {
handleUploadPreconditionFailedConflict(e, conflict);
} else {
Main.warn(tr("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
* {@link 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()) {
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);
}
}