source: josm/trunk/src/org/openstreetmap/josm/gui/io/AbstractUploadTask.java @ 5241

Revision 4816, 14.1 KB checked in by simon04, 4 months ago (diff)

fix #7257, fix #4093 - Provide an option to automatically download elements after a reference error

  • Property svn:eol-style set to native
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.io;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.event.ActionEvent;
8import java.net.HttpURLConnection;
9import java.text.SimpleDateFormat;
10import java.util.Arrays;
11import java.util.Collection;
12import java.util.Collections;
13import java.util.Date;
14import java.util.regex.Matcher;
15import java.util.regex.Pattern;
16
17import javax.swing.JOptionPane;
18
19import org.openstreetmap.josm.Main;
20import org.openstreetmap.josm.actions.DownloadReferrersAction;
21import org.openstreetmap.josm.actions.UpdateDataAction;
22import org.openstreetmap.josm.actions.UpdateSelectionAction;
23import org.openstreetmap.josm.data.osm.OsmPrimitive;
24import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
25import org.openstreetmap.josm.gui.ExceptionDialogUtil;
26import org.openstreetmap.josm.gui.HelpAwareOptionPane;
27import org.openstreetmap.josm.gui.PleaseWaitRunnable;
28import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
29import org.openstreetmap.josm.gui.layer.OsmDataLayer;
30import org.openstreetmap.josm.gui.progress.ProgressMonitor;
31import org.openstreetmap.josm.io.OsmApiException;
32import org.openstreetmap.josm.io.OsmApiInitializationException;
33import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
34import org.openstreetmap.josm.tools.DateUtils;
35import org.openstreetmap.josm.tools.ExceptionUtil;
36import org.openstreetmap.josm.tools.ImageProvider;
37import org.openstreetmap.josm.tools.Pair;
38
39public abstract class AbstractUploadTask extends PleaseWaitRunnable {
40    public AbstractUploadTask(String title, boolean ignoreException) {
41        super(title, ignoreException);
42    }
43
44    public AbstractUploadTask(String title, ProgressMonitor progressMonitor, boolean ignoreException) {
45        super(title, progressMonitor, ignoreException);
46    }
47
48    public AbstractUploadTask(String title) {
49        super(title);
50    }
51
52    /**
53     * Synchronizes the local state of an {@see OsmPrimitive} with its state on the
54     * server. The method uses an individual GET for the primitive.
55     *
56     * @param id the primitive ID
57     */
58    protected void synchronizePrimitive(final OsmPrimitiveType type, final long id) {
59        // FIXME: should now about the layer this task is running for. might
60        // be different from the current edit layer
61        OsmDataLayer layer = Main.main.getEditLayer();
62        if (layer == null)
63            throw new IllegalStateException(tr("Failed to update primitive with id {0} because current edit layer is null", id));
64        OsmPrimitive p = layer.data.getPrimitiveById(id, type);
65        if (p == null)
66            throw new IllegalStateException(tr("Failed to update primitive with id {0} because current edit layer does not include such a primitive", id));
67        Main.worker.execute(new UpdatePrimitivesTask(layer, Collections.singleton(p)));
68    }
69
70    /**
71     * Synchronizes the local state of the dataset with the state on the server.
72     *
73     * Reuses the functionality of {@see UpdateDataAction}.
74     *
75     * @see UpdateDataAction#actionPerformed(ActionEvent)
76     */
77    protected void synchronizeDataSet() {
78        UpdateDataAction act = new UpdateDataAction();
79        act.actionPerformed(new ActionEvent(this,0,""));
80    }
81
82    /**
83     * Handles the case that a conflict in a specific {@see OsmPrimitive} was detected while
84     * uploading
85     *
86     * @param primitiveType  the type of the primitive, either <code>node</code>, <code>way</code> or
87     *    <code>relation</code>
88     * @param id  the id of the primitive
89     * @param serverVersion  the version of the primitive on the server
90     * @param myVersion  the version of the primitive in the local dataset
91     */
92    protected void handleUploadConflictForKnownConflict(final OsmPrimitiveType primitiveType, final long id, String serverVersion, String myVersion) {
93        String lbl = "";
94        switch(primitiveType) {
95        case NODE: lbl =  tr("Synchronize node {0} only", id); break;
96        case WAY: lbl =  tr("Synchronize way {0} only", id); break;
97        case RELATION: lbl =  tr("Synchronize relation {0} only", id); break;
98        }
99        ButtonSpec[] spec = new ButtonSpec[] {
100                new ButtonSpec(
101                        lbl,
102                        ImageProvider.get("updatedata"),
103                        null,
104                        null
105                ),
106                new ButtonSpec(
107                        tr("Synchronize entire dataset"),
108                        ImageProvider.get("updatedata"),
109                        null,
110                        null
111                ),
112                new ButtonSpec(
113                        tr("Cancel"),
114                        ImageProvider.get("cancel"),
115                        null,
116                        null
117                )
118        };
119        String msg =  tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
120                + "of your nodes, ways, or relations.<br>"
121                + "The conflict is caused by the <strong>{0}</strong> with id <strong>{1}</strong>,<br>"
122                + "the server has version {2}, your version is {3}.<br>"
123                + "<br>"
124                + "Click <strong>{4}</strong> to synchronize the conflicting primitive only.<br>"
125                + "Click <strong>{5}</strong> to synchronize the entire local dataset with the server.<br>"
126                + "Click <strong>{6}</strong> to abort and continue editing.<br></html>",
127                tr(primitiveType.getAPIName()), id, serverVersion, myVersion,
128                spec[0].text, spec[1].text, spec[2].text
129        );
130        int ret = HelpAwareOptionPane.showOptionDialog(
131                Main.parent,
132                msg,
133                tr("Conflicts detected"),
134                JOptionPane.ERROR_MESSAGE,
135                null,
136                spec,
137                spec[0],
138                "/Concepts/Conflict"
139        );
140        switch(ret) {
141        case 0: synchronizePrimitive(primitiveType, id); break;
142        case 1: synchronizeDataSet(); break;
143        default: return;
144        }
145    }
146
147    /**
148     * Handles the case that a conflict was detected while uploading where we don't
149     * know what {@see OsmPrimitive} actually caused the conflict (for whatever reason)
150     *
151     */
152    protected void handleUploadConflictForUnknownConflict() {
153        ButtonSpec[] spec = new ButtonSpec[] {
154                new ButtonSpec(
155                        tr("Synchronize entire dataset"),
156                        ImageProvider.get("updatedata"),
157                        null,
158                        null
159                ),
160                new ButtonSpec(
161                        tr("Cancel"),
162                        ImageProvider.get("cancel"),
163                        null,
164                        null
165                )
166        };
167        String msg =  tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
168                + "of your nodes, ways, or relations.<br>"
169                + "<br>"
170                + "Click <strong>{0}</strong> to synchronize the entire local dataset with the server.<br>"
171                + "Click <strong>{1}</strong> to abort and continue editing.<br></html>",
172                spec[0].text, spec[1].text
173        );
174        int ret = HelpAwareOptionPane.showOptionDialog(
175                Main.parent,
176                msg,
177                tr("Conflicts detected"),
178                JOptionPane.ERROR_MESSAGE,
179                null,
180                spec,
181                spec[0],
182                ht("/Concepts/Conflict")
183        );
184        if (ret == 0) {
185            synchronizeDataSet();
186        }
187    }
188
189    /**
190     * Handles the case that a conflict was detected while uploading where we don't
191     * know what {@see OsmPrimitive} actually caused the conflict (for whatever reason)
192     *
193     */
194    protected void handleUploadConflictForClosedChangeset(long changesetId, Date d) {
195        String msg =  tr("<html>Uploading <strong>failed</strong> because you have been using<br>"
196                + "changeset {0} which was already closed at {1}.<br>"
197                + "Please upload again with a new or an existing open changeset.</html>",
198                changesetId, new SimpleDateFormat().format(d)
199        );
200        JOptionPane.showMessageDialog(
201                Main.parent,
202                msg,
203                tr("Changeset closed"),
204                JOptionPane.ERROR_MESSAGE
205        );
206    }
207
208    /**
209     * Handles the case where deleting a node failed because it is still in use in
210     * a non-deleted way on the server.
211     */
212    protected void handleUploadPreconditionFailedConflict(OsmApiException e, Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict) {
213        ButtonSpec[] options = new ButtonSpec[] {
214                new ButtonSpec(
215                        tr("Prepare conflict resolution"),
216                        ImageProvider.get("ok"),
217                        tr("Click to download all referring objects for {0}", conflict.a),
218                        null /* no specific help context */
219                ),
220                new ButtonSpec(
221                        tr("Cancel"),
222                        ImageProvider.get("cancel"),
223                        tr("Click to cancel and to resume editing the map"),
224                        null /* no specific help context */
225                )
226        };
227        String msg = ExceptionUtil.explainPreconditionFailed(e).replace("</html>", "<br><br>" + tr(
228                "Click <strong>{0}</strong> to load them now.<br>"
229                + "If necessary JOSM will create conflicts which you can resolve in the Conflict Resolution Dialog.",
230                options[0].text)) + "</html>";
231        int ret = HelpAwareOptionPane.showOptionDialog(
232                Main.parent,
233                msg,
234                tr("Object still in use"),
235                JOptionPane.ERROR_MESSAGE,
236                null,
237                options,
238                options[0],
239                "/Action/Upload#NodeStillInUseInWay"
240);
241        if (ret == 0) {
242            DownloadReferrersAction.downloadReferrers(Main.map.mapView.getEditLayer(), Arrays.asList(conflict.a));
243        }
244    }
245
246    /**
247     * handles an upload conflict, i.e. an error indicated by a HTTP return code 409.
248     *
249     * @param e  the exception
250     */
251    protected void handleUploadConflict(OsmApiException e) {
252        String pattern = "Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)";
253        Pattern p = Pattern.compile(pattern);
254        Matcher m = p.matcher(e.getErrorHeader());
255        if (m.matches()) {
256            handleUploadConflictForKnownConflict(OsmPrimitiveType.from(m.group(3)), Long.parseLong(m.group(4)), m.group(2),m.group(1));
257            return;
258        }
259        pattern ="The changeset (\\d+) was closed at (.*)";
260        p = Pattern.compile(pattern);
261        m = p.matcher(e.getErrorHeader());
262        if (m.matches()) {
263            handleUploadConflictForClosedChangeset(Long.parseLong(m.group(1)), DateUtils.fromString(m.group(2)));
264            return;
265        }
266        System.out.println(tr("Warning: error header \"{0}\" did not match with an expected pattern", e.getErrorHeader()));
267        handleUploadConflictForUnknownConflict();
268    }
269
270    /**
271     * handles an precondition failed conflict, i.e. an error indicated by a HTTP return code 412.
272     *
273     * @param e  the exception
274     */
275    protected void handlePreconditionFailed(OsmApiException e) {
276        // in the worst case, ExceptionUtil.parsePreconditionFailed is executed trice - should not be too expensive
277        Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict = ExceptionUtil.parsePreconditionFailed(e.getErrorHeader());
278        if (conflict != null) {
279            handleUploadPreconditionFailedConflict(e, conflict);
280        } else {
281            System.out.println(tr("Warning: error header \"{0}\" did not match with an expected pattern", e.getErrorHeader()));
282            ExceptionDialogUtil.explainPreconditionFailed(e);
283        }
284    }
285
286    /**
287     * Handles an error which is caused by a delete request for an already deleted
288     * {@see OsmPrimitive} on the server, i.e. a HTTP response code of 410.
289     * Note that an <strong>update</strong> on an already deleted object results
290     * in a 409, not a 410.
291     *
292     * @param e the exception
293     */
294    protected void handleGone(OsmApiPrimitiveGoneException e) {
295        if (e.isKnownPrimitive()) {
296            new UpdateSelectionAction().handlePrimitiveGoneException(e.getPrimitiveId(),e.getPrimitiveType());
297        } else {
298            ExceptionDialogUtil.explainGoneForUnknownPrimitive(e);
299        }
300    }
301
302    /**
303     * error handler for any exception thrown during upload
304     *
305     * @param e the exception
306     */
307    protected void handleFailedUpload(Exception e) {
308        // API initialization failed. Notify the user and return.
309        //
310        if (e instanceof OsmApiInitializationException) {
311            ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException)e);
312            return;
313        }
314
315        if (e instanceof OsmApiPrimitiveGoneException) {
316            handleGone((OsmApiPrimitiveGoneException)e);
317            return;
318        }
319        if (e instanceof OsmApiException) {
320            OsmApiException ex = (OsmApiException)e;
321            // There was an upload conflict. Let the user decide whether
322            // and how to resolve it
323            //
324            if(ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) {
325                handleUploadConflict(ex);
326                return;
327            }
328            // There was a precondition failed. Notify the user.
329            //
330            else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) {
331                handlePreconditionFailed(ex);
332                return;
333            }
334            // Tried to update or delete a primitive which never existed on
335            // the server?
336            //
337            else if (ex.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
338                ExceptionDialogUtil.explainNotFound(ex);
339                return;
340            }
341        }
342
343        ExceptionDialogUtil.explainException(e);
344    }
345}
Note: See TracBrowser for help on using the repository browser.