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

Last change on this file since 15586 was 15586, checked in by Don-vip, 4 years ago

code cleanup

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