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

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

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

  • Property svn:eol-style set to native
File size: 15.5 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.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.HelpAwareOptionPane.ButtonSpec;
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 = Main.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 Main.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 = new ButtonSpec[] {
131 new ButtonSpec(
132 lbl,
133 ImageProvider.get("updatedata"),
134 null,
135 null
136 ),
137 new ButtonSpec(
138 tr("Synchronize entire dataset"),
139 ImageProvider.get("updatedata"),
140 null,
141 null
142 ),
143 new ButtonSpec(
144 tr("Cancel"),
145 ImageProvider.get("cancel"),
146 null,
147 null
148 )
149 };
150 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
151 + "of your nodes, ways, or relations.<br>"
152 + "The conflict is caused by the <strong>{0}</strong> with id <strong>{1}</strong>,<br>"
153 + "the server has version {2}, your version is {3}.<br>"
154 + "<br>"
155 + "Click <strong>{4}</strong> to synchronize the conflicting primitive only.<br>"
156 + "Click <strong>{5}</strong> to synchronize the entire local dataset with the server.<br>"
157 + "Click <strong>{6}</strong> to abort and continue editing.<br></html>",
158 tr(primitiveType.getAPIName()), id, serverVersion, myVersion,
159 spec[0].text, spec[1].text, spec[2].text
160 );
161 int ret = HelpAwareOptionPane.showOptionDialog(
162 Main.parent,
163 msg,
164 tr("Conflicts detected"),
165 JOptionPane.ERROR_MESSAGE,
166 null,
167 spec,
168 spec[0],
169 "/Concepts/Conflict"
170 );
171 switch(ret) {
172 case 0: synchronizePrimitive(primitiveType, id); break;
173 case 1: synchronizeDataSet(); break;
174 default: return;
175 }
176 }
177
178 /**
179 * Handles the case that a conflict was detected while uploading where we don't
180 * know what {@link OsmPrimitive} actually caused the conflict (for whatever reason)
181 *
182 */
183 protected void handleUploadConflictForUnknownConflict() {
184 ButtonSpec[] spec = new ButtonSpec[] {
185 new ButtonSpec(
186 tr("Synchronize entire dataset"),
187 ImageProvider.get("updatedata"),
188 null,
189 null
190 ),
191 new ButtonSpec(
192 tr("Cancel"),
193 ImageProvider.get("cancel"),
194 null,
195 null
196 )
197 };
198 String msg = tr("<html>Uploading <strong>failed</strong> because the server has a newer version of one<br>"
199 + "of your nodes, ways, or relations.<br>"
200 + "<br>"
201 + "Click <strong>{0}</strong> to synchronize the entire local dataset with the server.<br>"
202 + "Click <strong>{1}</strong> to abort and continue editing.<br></html>",
203 spec[0].text, spec[1].text
204 );
205 int ret = HelpAwareOptionPane.showOptionDialog(
206 Main.parent,
207 msg,
208 tr("Conflicts detected"),
209 JOptionPane.ERROR_MESSAGE,
210 null,
211 spec,
212 spec[0],
213 ht("/Concepts/Conflict")
214 );
215 if (ret == 0) {
216 synchronizeDataSet();
217 }
218 }
219
220 /**
221 * Handles the case that a conflict was detected while uploading where we don't
222 * know what {@link OsmPrimitive} actually caused the conflict (for whatever reason)
223 * @param changesetId changeset ID
224 * @param d changeset date
225 */
226 protected void handleUploadConflictForClosedChangeset(long changesetId, Date d) {
227 String msg = tr("<html>Uploading <strong>failed</strong> because you have been using<br>"
228 + "changeset {0} which was already closed at {1}.<br>"
229 + "Please upload again with a new or an existing open changeset.</html>",
230 changesetId, DateUtils.formatDateTime(d, DateFormat.SHORT, DateFormat.SHORT)
231 );
232 JOptionPane.showMessageDialog(
233 Main.parent,
234 msg,
235 tr("Changeset closed"),
236 JOptionPane.ERROR_MESSAGE
237 );
238 }
239
240 /**
241 * Handles the case where deleting a node failed because it is still in use in
242 * a non-deleted way on the server.
243 * @param e exception
244 * @param conflict conflict
245 */
246 protected void handleUploadPreconditionFailedConflict(OsmApiException e, Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict) {
247 ButtonSpec[] options = new ButtonSpec[] {
248 new ButtonSpec(
249 tr("Prepare conflict resolution"),
250 ImageProvider.get("ok"),
251 tr("Click to download all referring objects for {0}", conflict.a),
252 null /* no specific help context */
253 ),
254 new ButtonSpec(
255 tr("Cancel"),
256 ImageProvider.get("cancel"),
257 tr("Click to cancel and to resume editing the map"),
258 null /* no specific help context */
259 )
260 };
261 String msg = ExceptionUtil.explainPreconditionFailed(e).replace("</html>", "<br><br>" + tr(
262 "Click <strong>{0}</strong> to load them now.<br>"
263 + "If necessary JOSM will create conflicts which you can resolve in the Conflict Resolution Dialog.",
264 options[0].text)) + "</html>";
265 int ret = HelpAwareOptionPane.showOptionDialog(
266 Main.parent,
267 msg,
268 tr("Object still in use"),
269 JOptionPane.ERROR_MESSAGE,
270 null,
271 options,
272 options[0],
273 "/Action/Upload#NodeStillInUseInWay"
274 );
275 if (ret == 0) {
276 DownloadReferrersAction.downloadReferrers(Main.getLayerManager().getEditLayer(), Arrays.asList(conflict.a));
277 }
278 }
279
280 /**
281 * handles an upload conflict, i.e. an error indicated by a HTTP return code 409.
282 *
283 * @param e the exception
284 */
285 protected void handleUploadConflict(OsmApiException e) {
286 final String errorHeader = e.getErrorHeader();
287 if (errorHeader != null) {
288 Pattern p = Pattern.compile("Version mismatch: Provided (\\d+), server had: (\\d+) of (\\S+) (\\d+)");
289 Matcher m = p.matcher(errorHeader);
290 if (m.matches()) {
291 handleUploadConflictForKnownConflict(OsmPrimitiveType.from(m.group(3)), Long.parseLong(m.group(4)), m.group(2), m.group(1));
292 return;
293 }
294 p = Pattern.compile("The changeset (\\d+) was closed at (.*)");
295 m = p.matcher(errorHeader);
296 if (m.matches()) {
297 handleUploadConflictForClosedChangeset(Long.parseLong(m.group(1)), DateUtils.fromString(m.group(2)));
298 return;
299 }
300 }
301 Logging.warn(tr("Error header \"{0}\" did not match with an expected pattern", errorHeader));
302 handleUploadConflictForUnknownConflict();
303 }
304
305 /**
306 * handles an precondition failed conflict, i.e. an error indicated by a HTTP return code 412.
307 *
308 * @param e the exception
309 */
310 protected void handlePreconditionFailed(OsmApiException e) {
311 // in the worst case, ExceptionUtil.parsePreconditionFailed is executed trice - should not be too expensive
312 Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict = ExceptionUtil.parsePreconditionFailed(e.getErrorHeader());
313 if (conflict != null) {
314 handleUploadPreconditionFailedConflict(e, conflict);
315 } else {
316 Logging.warn(tr("Error header \"{0}\" did not match with an expected pattern", e.getErrorHeader()));
317 ExceptionDialogUtil.explainPreconditionFailed(e);
318 }
319 }
320
321 /**
322 * Handles an error which is caused by a delete request for an already deleted
323 * {@link OsmPrimitive} on the server, i.e. a HTTP response code of 410.
324 * Note that an <strong>update</strong> on an already deleted object results
325 * in a 409, not a 410.
326 *
327 * @param e the exception
328 */
329 protected void handleGone(OsmApiPrimitiveGoneException e) {
330 if (e.isKnownPrimitive()) {
331 UpdateSelectionAction.handlePrimitiveGoneException(e.getPrimitiveId(), e.getPrimitiveType());
332 } else {
333 ExceptionDialogUtil.explainGoneForUnknownPrimitive(e);
334 }
335 }
336
337 /**
338 * error handler for any exception thrown during upload
339 *
340 * @param e the exception
341 */
342 protected void handleFailedUpload(Exception e) {
343 // API initialization failed. Notify the user and return.
344 //
345 if (e instanceof OsmApiInitializationException) {
346 ExceptionDialogUtil.explainOsmApiInitializationException((OsmApiInitializationException) e);
347 return;
348 }
349
350 if (e instanceof OsmApiPrimitiveGoneException) {
351 handleGone((OsmApiPrimitiveGoneException) e);
352 return;
353 }
354 if (e instanceof OsmApiException) {
355 OsmApiException ex = (OsmApiException) e;
356 if (ex.getResponseCode() == HttpURLConnection.HTTP_CONFLICT) {
357 // There was an upload conflict. Let the user decide whether and how to resolve it
358 handleUploadConflict(ex);
359 return;
360 } else if (ex.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) {
361 // There was a precondition failed. Notify the user.
362 handlePreconditionFailed(ex);
363 return;
364 } else if (ex.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) {
365 // Tried to update or delete a primitive which never existed on the server?
366 ExceptionDialogUtil.explainNotFound(ex);
367 return;
368 }
369 }
370
371 ExceptionDialogUtil.explainException(e);
372 }
373}
Note: See TracBrowser for help on using the repository browser.