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