// 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;
/**
* Abstract base class for the task of uploading primitives via OSM API.
*
* Mainly handles conflicts and certain error situations.
*/
public abstract class AbstractUploadTask extends PleaseWaitRunnable {
/**
* Constructs a new {@code AbstractUploadTask}.
* @param title message for the user
* @param ignoreException If true, exception will be silently ignored. If false then
* exception will be handled by showing a dialog. When this runnable is executed using executor framework
* then use false unless you read result of task (because exception will get lost if you don't)
*/
public AbstractUploadTask(String title, boolean ignoreException) {
super(title, ignoreException);
}
/**
* Constructs a new {@code AbstractUploadTask}.
* @param title message for the user
* @param progressMonitor progress monitor
* @param ignoreException If true, exception will be silently ignored. If false then
* exception will be handled by showing a dialog. When this runnable is executed using executor framework
* then use false unless you read result of task (because exception will get lost if you don't)
*/
public AbstractUploadTask(String title, ProgressMonitor progressMonitor, boolean ignoreException) {
super(title, progressMonitor, ignoreException);
}
/**
* Constructs a new {@code AbstractUploadTask}.
* @param title message for the user
*/
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 type the primitive type
* @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.getLayerManager().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 node
, way
or
* relation
* @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) {
// CHECKSTYLE.OFF: SingleSpaceSeparator
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;
// CHECKSTYLE.ON: SingleSpaceSeparator
default: throw new AssertionError();
}
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("Uploading failed because the server has a newer version of one
"
+ "of your nodes, ways, or relations.
"
+ "The conflict is caused by the {0} with id {1},
"
+ "the server has version {2}, your version is {3}.
"
+ "
"
+ "Click {4} to synchronize the conflicting primitive only.
"
+ "Click {5} to synchronize the entire local dataset with the server.
"
+ "Click {6} to abort and continue editing.
",
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("Uploading failed because the server has a newer version of one
"
+ "of your nodes, ways, or relations.
"
+ "
"
+ "Click {0} to synchronize the entire local dataset with the server.
"
+ "Click {1} to abort and continue editing.
",
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)
* @param changesetId changeset ID
* @param d changeset date
*/
protected void handleUploadConflictForClosedChangeset(long changesetId, Date d) {
String msg = tr("Uploading failed because you have been using
"
+ "changeset {0} which was already closed at {1}.
"
+ "Please upload again with a new or an existing open changeset.",
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.
* @param e exception
* @param conflict conflict
*/
protected void handleUploadPreconditionFailedConflict(OsmApiException e, Pair> 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("