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