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

Last change on this file was 19106, checked in by taylor.smock, 2 years ago

Cleanup some new PMD warnings from PMD 7.x (followup of r19101)

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