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