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