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     *
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     *
195     */
196    protected void handleUploadConflictForClosedChangeset(long changesetId, Date d) {
197        String msg =  tr("<html>Uploading <strong>failed</strong> because you have been using<br>"
198                + "changeset {0} which was already closed at {1}.<br>"
199                + "Please upload again with a new or an existing open changeset.</html>",
200                changesetId, DateUtils.formatDateTime(d, DateFormat.SHORT, DateFormat.SHORT)
201        );
202        JOptionPane.showMessageDialog(
203                Main.parent,
204                msg,
205                tr("Changeset closed"),
206                JOptionPane.ERROR_MESSAGE
207        );
208    }
209
210    /**
211     * Handles the case where deleting a node failed because it is still in use in
212     * a non-deleted way on the server.
213     */
214    protected void handleUploadPreconditionFailedConflict(OsmApiException e, Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict) {
215        ButtonSpec[] options = new ButtonSpec[] {
216                new ButtonSpec(
217                        tr("Prepare conflict resolution"),
218                        ImageProvider.get("ok"),
219                        tr("Click to download all referring objects for {0}", conflict.a),
220                        null /* no specific help context */
221                ),
222                new ButtonSpec(
223                        tr("Cancel"),
224                        ImageProvider.get("cancel"),
225                        tr("Click to cancel and to resume editing the map"),
226                        null /* no specific help context */
227                )
228        };
229        String msg = ExceptionUtil.explainPreconditionFailed(e).replace("</html>", "<br><br>" + tr(
230                "Click <strong>{0}</strong> to load them now.<br>"
231                + "If necessary JOSM will create conflicts which you can resolve in the Conflict Resolution Dialog.",
232                options[0].text)) + "</html>";
233        int ret = HelpAwareOptionPane.showOptionDialog(
234                Main.parent,
235                msg,
236                tr("Object still in use"),
237                JOptionPane.ERROR_MESSAGE,
238                null,
239                options,
240                options[0],
241                "/Action/Upload#NodeStillInUseInWay"
242);
243        if (ret == 0) {
244            DownloadReferrersAction.downloadReferrers(Main.main.getEditLayer(), Arrays.asList(conflict.a));
245        }
246    }
247
248    /**
249     * handles an upload conflict, i.e. an error indicated by a HTTP return code 409.
250     *
251     * @param e  the exception
252     */
253    protected void handleUploadConflict(OsmApiException e) {
254        final String errorHeader = e.getErrorHeader();
255        if (errorHeader != null) {
256            Pattern p = Pattern.compile("Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)");
257            Matcher m = p.matcher(errorHeader);
258            if (m.matches()) {
259                handleUploadConflictForKnownConflict(OsmPrimitiveType.from(m.group(3)), Long.parseLong(m.group(4)), m.group(2), m.group(1));
260                return;
261            }
262            p = Pattern.compile("The changeset (\\d+) was closed at (.*)");
263            m = p.matcher(errorHeader);
264            if (m.matches()) {
265                handleUploadConflictForClosedChangeset(Long.parseLong(m.group(1)), DateUtils.fromString(m.group(2)));
266                return;
267            }
268        }
269        Main.warn(tr("Error header \"{0}\" did not match with an expected pattern", errorHeader));
270        handleUploadConflictForUnknownConflict();
271    }
272
273    /**
274     * handles an precondition failed conflict, i.e. an error indicated by a HTTP return code 412.
275     *
276     * @param e  the exception
277     */
278    protected void handlePreconditionFailed(OsmApiException e) {
279        // in the worst case, ExceptionUtil.parsePreconditionFailed is executed trice - should not be too expensive
280        Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict = ExceptionUtil.parsePreconditionFailed(e.getErrorHeader());
281        if (conflict != null) {
282            handleUploadPreconditionFailedConflict(e, conflict);
283        } else {
284            Main.warn(tr("Error header \"{0}\" did not match with an expected pattern", e.getErrorHeader()));
285            ExceptionDialogUtil.explainPreconditionFailed(e);
286        }
287    }
288
289    /**
290     * Handles an error which is caused by a delete request for an already deleted
291     * {@link OsmPrimitive} on the server, i.e. a HTTP response code of 410.
292     * Note that an <strong>update</strong> on an already deleted object results
293     * in a 409, not a 410.
294     *
295     * @param e the exception
296     */
297    protected void handleGone(OsmApiPrimitiveGoneException e) {
298        if (e.isKnownPrimitive()) {
299            UpdateSelectionAction.handlePrimitiveGoneException(e.getPrimitiveId(), e.getPrimitiveType());
300        } else {
301            ExceptionDialogUtil.explainGoneForUnknownPrimitive(e);
302        }
303    }
304
305    /**
306     * error handler for any exception thrown during upload
307     *
308     * @param e the exception
309     */
310    protected void handleFailedUpload(Exception e) {
311        // API initialization failed. Notify the user and return.
312        //
313        if (e instanceof OsmApiInitializationException) {
314            ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException) e);
315            return;
316        }
317
318        if (e instanceof OsmApiPrimitiveGoneException) {
319            handleGone((OsmApiPrimitiveGoneException) e);
320            return;
321        }
322        if (e instanceof OsmApiException) {
323            OsmApiException ex = (OsmApiException) e;
324            if (ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) {
325                // There was an upload conflict. Let the user decide whether and how to resolve it
326                handleUploadConflict(ex);
327                return;
328            } else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) {
329                // There was a precondition failed. Notify the user.
330                handlePreconditionFailed(ex);
331                return;
332            } else if (ex.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
333                // Tried to update or delete a primitive which never existed on the server?
334                ExceptionDialogUtil.explainNotFound(ex);
335                return;
336            }
337        }
338
339        ExceptionDialogUtil.explainException(e);
340    }
341}