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