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