001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.io;
003    
004    import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005    import static org.openstreetmap.josm.tools.I18n.tr;
006    
007    import java.awt.event.ActionEvent;
008    import java.net.HttpURLConnection;
009    import java.text.DateFormat;
010    import java.util.Arrays;
011    import java.util.Collection;
012    import java.util.Collections;
013    import java.util.Date;
014    import java.util.regex.Matcher;
015    import java.util.regex.Pattern;
016    
017    import javax.swing.JOptionPane;
018    
019    import org.openstreetmap.josm.Main;
020    import org.openstreetmap.josm.actions.DownloadReferrersAction;
021    import org.openstreetmap.josm.actions.UpdateDataAction;
022    import org.openstreetmap.josm.actions.UpdateSelectionAction;
023    import org.openstreetmap.josm.data.osm.OsmPrimitive;
024    import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
025    import org.openstreetmap.josm.gui.ExceptionDialogUtil;
026    import org.openstreetmap.josm.gui.HelpAwareOptionPane;
027    import org.openstreetmap.josm.gui.PleaseWaitRunnable;
028    import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
029    import org.openstreetmap.josm.gui.layer.OsmDataLayer;
030    import org.openstreetmap.josm.gui.progress.ProgressMonitor;
031    import org.openstreetmap.josm.io.OsmApiException;
032    import org.openstreetmap.josm.io.OsmApiInitializationException;
033    import org.openstreetmap.josm.io.OsmApiPrimitiveGoneException;
034    import org.openstreetmap.josm.tools.DateUtils;
035    import org.openstreetmap.josm.tools.ExceptionUtil;
036    import org.openstreetmap.josm.tools.ImageProvider;
037    import org.openstreetmap.josm.tools.Pair;
038    
039    public 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(tr("Failed to update primitive with id {0} because current edit layer does not include such a primitive", id));
067            Main.worker.execute(new UpdatePrimitivesTask(layer, Collections.singleton(p)));
068        }
069    
070        /**
071         * Synchronizes the local state of the dataset with the state on the server.
072         *
073         * Reuses the functionality of {@link UpdateDataAction}.
074         *
075         * @see UpdateDataAction#actionPerformed(ActionEvent)
076         */
077        protected void synchronizeDataSet() {
078            UpdateDataAction act = new UpdateDataAction();
079            act.actionPerformed(new ActionEvent(this,0,""));
080        }
081    
082        /**
083         * Handles the case that a conflict in a specific {@link OsmPrimitive} was detected while
084         * uploading
085         *
086         * @param primitiveType  the type of the primitive, either <code>node</code>, <code>way</code> or
087         *    <code>relation</code>
088         * @param id  the id of the primitive
089         * @param serverVersion  the version of the primitive on the server
090         * @param myVersion  the version of the primitive in the local dataset
091         */
092        protected void handleUploadConflictForKnownConflict(final OsmPrimitiveType primitiveType, final long id, String serverVersion, String myVersion) {
093            String lbl = "";
094            switch(primitiveType) {
095            case NODE: lbl =  tr("Synchronize node {0} only", id); break;
096            case WAY: lbl =  tr("Synchronize way {0} only", id); break;
097            case RELATION: lbl =  tr("Synchronize relation {0} only", id); break;
098            }
099            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 {@link 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 {@link 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, DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).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         * {@link 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    }