001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.io.IOException; 008import java.net.HttpURLConnection; 009import java.net.MalformedURLException; 010import java.net.SocketException; 011import java.net.URL; 012import java.net.UnknownHostException; 013import java.time.Instant; 014import java.time.ZoneId; 015import java.time.format.FormatStyle; 016import java.util.Arrays; 017import java.util.Collection; 018import java.util.List; 019import java.util.Objects; 020import java.util.Optional; 021import java.util.TreeSet; 022import java.util.regex.Matcher; 023import java.util.regex.Pattern; 024 025import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder; 026import org.openstreetmap.josm.data.osm.Node; 027import org.openstreetmap.josm.data.osm.OsmPrimitive; 028import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 029import org.openstreetmap.josm.data.osm.Relation; 030import org.openstreetmap.josm.data.osm.Way; 031import org.openstreetmap.josm.io.ChangesetClosedException; 032import org.openstreetmap.josm.io.IllegalDataException; 033import org.openstreetmap.josm.io.MissingOAuthAccessTokenException; 034import org.openstreetmap.josm.io.OfflineAccessException; 035import org.openstreetmap.josm.io.OsmApi; 036import org.openstreetmap.josm.io.OsmApiException; 037import org.openstreetmap.josm.io.OsmApiInitializationException; 038import org.openstreetmap.josm.io.OsmTransferException; 039import org.openstreetmap.josm.io.auth.CredentialsManager; 040import org.openstreetmap.josm.tools.date.DateUtils; 041 042/** 043 * Utilities for exception handling. 044 * @since 2097 045 */ 046public final class ExceptionUtil { 047 048 /** 049 * Error messages sent by the OSM API when a user has been blocked/suspended. 050 */ 051 private static final List<String> OSM_API_BLOCK_MESSAGES = Arrays.asList( 052 "You have an urgent message on the OpenStreetMap web site. " + 053 "You need to read the message before you will be able to save your edits.", 054 "Your access to the API has been blocked. Please log-in to the web interface to find out more.", 055 "Your access to the API is temporarily suspended. Please log-in to the web interface to view the Contributor Terms." + 056 " You do not need to agree, but you must view them."); 057 058 private ExceptionUtil() { 059 // Hide default constructor for utils classes 060 } 061 062 /** 063 * Explains an exception caught during OSM API initialization. 064 * 065 * @param e the exception 066 * @return The HTML formatted error message to display 067 */ 068 public static String explainOsmApiInitializationException(OsmApiInitializationException e) { 069 Logging.error(e); 070 return tr( 071 "<html>Failed to initialize communication with the OSM server {0}.<br>" 072 + "Check the server URL in your preferences and your internet connection.", 073 OsmApi.getOsmApi().getServerUrl())+"</html>"; 074 } 075 076 /** 077 * Explains a {@link OsmApiException} which was thrown because accessing a protected 078 * resource was forbidden. 079 * 080 * @param e the exception 081 * @return The HTML formatted error message to display 082 */ 083 public static String explainMissingOAuthAccessTokenException(MissingOAuthAccessTokenException e) { 084 Logging.error(e); 085 return tr( 086 "<html>Failed to authenticate at the OSM server ''{0}''.<br>" 087 + "You are using OAuth to authenticate but currently there is no<br>" 088 + "OAuth Access Token configured.<br>" 089 + "Please open the Preferences Dialog and generate or enter an Access Token." 090 + "</html>", 091 OsmApi.getOsmApi().getServerUrl() 092 ); 093 } 094 095 /** 096 * Parses a precondition failure response from the server and attempts to get more information about it 097 * @param msg The message from the server 098 * @return The OSM primitive that caused the problem and a collection of primitives that e.g. refer to it 099 */ 100 public static Pair<OsmPrimitive, Collection<OsmPrimitive>> parsePreconditionFailed(String msg) { 101 if (msg == null) 102 return null; 103 final String ids = "(\\d+(?:,\\d+)*)"; 104 final Collection<OsmPrimitive> refs = new TreeSet<>(); // error message can contain several times the same way 105 Matcher m; 106 m = Pattern.compile(".*Node (\\d+) is still used by relations? " + ids + ".*").matcher(msg); 107 if (m.matches()) { 108 OsmPrimitive n = new Node(Long.parseLong(m.group(1))); 109 for (String s : m.group(2).split(",", -1)) { 110 refs.add(new Relation(Long.parseLong(s))); 111 } 112 return Pair.create(n, refs); 113 } 114 m = Pattern.compile(".*Node (\\d+) is still used by ways? " + ids + ".*").matcher(msg); 115 if (m.matches()) { 116 OsmPrimitive n = new Node(Long.parseLong(m.group(1))); 117 for (String s : m.group(2).split(",", -1)) { 118 refs.add(new Way(Long.parseLong(s))); 119 } 120 return Pair.create(n, refs); 121 } 122 m = Pattern.compile(".*The relation (\\d+) is used in relations? " + ids + ".*").matcher(msg); 123 if (m.matches()) { 124 OsmPrimitive n = new Relation(Long.parseLong(m.group(1))); 125 for (String s : m.group(2).split(",", -1)) { 126 refs.add(new Relation(Long.parseLong(s))); 127 } 128 return Pair.create(n, refs); 129 } 130 m = Pattern.compile(".*Way (\\d+) is still used by relations? " + ids + ".*").matcher(msg); 131 if (m.matches()) { 132 OsmPrimitive n = new Way(Long.parseLong(m.group(1))); 133 for (String s : m.group(2).split(",", -1)) { 134 refs.add(new Relation(Long.parseLong(s))); 135 } 136 return Pair.create(n, refs); 137 } 138 m = Pattern.compile(".*Way ([-]*\\d+) requires the nodes with id in " + ids + ".*").matcher(msg); 139 // ... ", which either do not exist, or are not visible" 140 if (m.matches()) { 141 OsmPrimitive n = OsmPrimitiveType.WAY.newInstance(Long.parseLong(m.group(1)), true); 142 for (String s : m.group(2).split(",", -1)) { 143 refs.add(new Node(Long.parseLong(s))); 144 } 145 return Pair.create(n, refs); 146 } 147 m = Pattern.compile(".*Relation ([-]*\\d+) requires the nodes with id in " + ids + ".*").matcher(msg); 148 // ... ", which either do not exist, or are not visible" 149 if (m.matches()) { 150 OsmPrimitive n = OsmPrimitiveType.RELATION.newInstance(Long.parseLong(m.group(1)), true); 151 for (String s : m.group(2).split(",", -1)) { 152 refs.add(new Node(Long.parseLong(s))); 153 } 154 return Pair.create(n, refs); 155 } 156 m = Pattern.compile(".*Relation ([-]*\\d+) requires the ways with id in " + ids + ".*").matcher(msg); 157 // ... ", which either do not exist, or are not visible" 158 if (m.matches()) { 159 OsmPrimitive n = OsmPrimitiveType.RELATION.newInstance(Long.parseLong(m.group(1)), true); 160 for (String s : m.group(2).split(",", -1)) { 161 refs.add(new Way(Long.parseLong(s))); 162 } 163 return Pair.create(n, refs); 164 } 165 return null; 166 } 167 168 /** 169 * Explains an upload error due to a violated precondition, i.e. a HTTP return code 412 170 * 171 * @param e the exception 172 * @return The HTML formatted error message to display 173 */ 174 public static String explainPreconditionFailed(OsmApiException e) { 175 Logging.error(e); 176 Pair<OsmPrimitive, Collection<OsmPrimitive>> conflict = parsePreconditionFailed(e.getErrorHeader()); 177 if (conflict != null) { 178 OsmPrimitive firstRefs = conflict.b.iterator().next(); 179 String objId = Long.toString(conflict.a.getUniqueId()); 180 Collection<Long> refIds = Utils.transform(conflict.b, OsmPrimitive::getId); 181 String refIdsString = refIds.size() == 1 ? refIds.iterator().next().toString() : refIds.toString(); 182 if (conflict.a instanceof Node) { 183 if (firstRefs instanceof Way) { 184 return "<html>" + trn( 185 "<strong>Failed</strong> to delete <strong>node {0}</strong>." 186 + " It is still referred to by way {1}.<br>" 187 + "Please load the way, remove the reference to the node, and upload again.", 188 "<strong>Failed</strong> to delete <strong>node {0}</strong>." 189 + " It is still referred to by ways {1}.<br>" 190 + "Please load the ways, remove the reference to the node, and upload again.", 191 conflict.b.size(), objId, refIdsString) + "</html>"; 192 } else if (firstRefs instanceof Relation) { 193 return "<html>" + trn( 194 "<strong>Failed</strong> to delete <strong>node {0}</strong>." 195 + " It is still referred to by relation {1}.<br>" 196 + "Please load the relation, remove the reference to the node, and upload again.", 197 "<strong>Failed</strong> to delete <strong>node {0}</strong>." 198 + " It is still referred to by relations {1}.<br>" 199 + "Please load the relations, remove the reference to the node, and upload again.", 200 conflict.b.size(), objId, refIdsString) + "</html>"; 201 } else { 202 throw new IllegalStateException(); 203 } 204 } else if (conflict.a instanceof Way) { 205 if (firstRefs instanceof Node) { 206 // way p1 requires nodes 207 return "<html>" + trn( 208 "<strong>Failed</strong> to upload <strong>way {0}</strong>." 209 + " It refers to deleted node {1}.<br>" 210 + "Please load the node, remove the reference in the way, and upload again.", 211 "<strong>Failed</strong> to upload <strong>way {0}</strong>." 212 + " It refers to deleted nodes {1}.<br>" 213 + "Please load the nodes, remove the reference in the way, and upload again.", 214 conflict.b.size(), objId, refIdsString) + "</html>"; 215 } else if (firstRefs instanceof Relation) { 216 // way is used by relation 217 return "<html>" + trn( 218 "<strong>Failed</strong> to delete <strong>way {0}</strong>." 219 + " It is still referred to by relation {1}.<br>" 220 + "Please load the relation, remove the reference to the way, and upload again.", 221 "<strong>Failed</strong> to delete <strong>way {0}</strong>." 222 + " It is still referred to by relations {1}.<br>" 223 + "Please load the relations, remove the reference to the way, and upload again.", 224 conflict.b.size(), objId, refIdsString) + "</html>"; 225 } else { 226 throw new IllegalStateException(); 227 } 228 } else if (conflict.a instanceof Relation) { 229 if (firstRefs instanceof Node) { 230 return "<html>" + trn( 231 "<strong>Failed</strong> to upload <strong>relation {0}</strong>." 232 + " it refers to deleted node {1}.<br>" 233 + "Please load the node, remove the reference in the relation, and upload again.", 234 "<strong>Failed</strong> to upload <strong>relation {0}</strong>." 235 + " it refers to deleted nodes {1}.<br>" 236 + "Please load the nodes, remove the reference in the relation, and upload again.", 237 conflict.b.size(), objId, refIdsString) + "</html>"; 238 } else if (firstRefs instanceof Way) { 239 return "<html>" + trn( 240 "<strong>Failed</strong> to upload <strong>relation {0}</strong>." 241 + " It refers to deleted way {1}.<br>" 242 + "Please load the way, remove the reference in the relation, and upload again.", 243 "<strong>Failed</strong> to upload <strong>relation {0}</strong>." 244 + " It refers to deleted ways {1}.<br>" 245 + "Please load the ways, remove the reference in the relation, and upload again.", 246 conflict.b.size(), objId, refIdsString) + "</html>"; 247 } else if (firstRefs instanceof Relation) { 248 return "<html>" + trn( 249 "<strong>Failed</strong> to delete <strong>relation {0}</strong>." 250 + " It is still referred to by relation {1}.<br>" 251 + "Please load the relation, remove the reference to the relation, and upload again.", 252 "<strong>Failed</strong> to delete <strong>relation {0}</strong>." 253 + " It is still referred to by relations {1}.<br>" 254 + "Please load the relations, remove the reference to the relation, and upload again.", 255 conflict.b.size(), objId, refIdsString) + "</html>"; 256 } else { 257 throw new IllegalStateException(); 258 } 259 } else { 260 throw new IllegalStateException(); 261 } 262 } else { 263 return tr( 264 "<html>Uploading to the server <strong>failed</strong> because your current<br>" 265 + "dataset violates a precondition.<br>" + "The error message is:<br>" + "{0}" + "</html>", 266 Utils.escapeReservedCharactersHTML(e.getMessage())); 267 } 268 } 269 270 /** 271 * Explains a {@link OsmApiException} which was thrown because the authentication at 272 * the OSM server failed, with basic authentication. 273 * 274 * @param e the exception 275 * @return The HTML formatted error message to display 276 */ 277 public static String explainFailedBasicAuthentication(OsmApiException e) { 278 Logging.error(e); 279 return tr("<html>" 280 + "Authentication at the OSM server with the username ''{0}'' failed.<br>" 281 + "Please check the username and the password in the JOSM preferences." 282 + "</html>", 283 e.getLogin() != null ? e.getLogin() : CredentialsManager.getInstance().getUsername() 284 ); 285 } 286 287 /** 288 * Explains a {@link OsmApiException} which was thrown because the authentication at 289 * the OSM server failed, with OAuth authentication. 290 * 291 * @param e the exception 292 * @return The HTML formatted error message to display 293 */ 294 public static String explainFailedOAuthAuthentication(OsmApiException e) { 295 Logging.error(e); 296 return tr("<html>" 297 + "Authentication at the OSM server with the OAuth token ''{0}'' failed.<br>" 298 + "Please launch the preferences dialog and retrieve another OAuth token." 299 + "</html>", 300 OAuthAccessTokenHolder.getInstance().getAccessTokenKey() 301 ); 302 } 303 304 /** 305 * Explains a {@link OsmApiException} which was thrown because accessing a protected 306 * resource was forbidden (HTTP 403), without OAuth authentication. 307 * 308 * @param e the exception 309 * @return The HTML formatted error message to display 310 */ 311 public static String explainFailedAuthorisation(OsmApiException e) { 312 Logging.error(e); 313 String msg = e.getDisplayMessage(); 314 315 if (!Utils.isEmpty(msg)) { 316 return tr("<html>" 317 + "Authorisation at the OSM server failed.<br>" 318 + "The server reported the following error:<br>" 319 + "''{0}''" 320 + "</html>", 321 msg 322 ); 323 } else { 324 return tr("<html>" 325 + "Authorisation at the OSM server failed.<br>" 326 + "</html>" 327 ); 328 } 329 } 330 331 /** 332 * Explains a {@link OsmApiException} which was thrown because accessing a protected 333 * resource was forbidden (HTTP 403), with OAuth authentication. 334 * 335 * @param e the exception 336 * @return The HTML formatted error message to display 337 */ 338 public static String explainFailedOAuthAuthorisation(OsmApiException e) { 339 Logging.error(e); 340 return tr("<html>" 341 + "Authorisation at the OSM server with the OAuth token ''{0}'' failed.<br>" 342 + "The token is not authorised to access the protected resource<br>" 343 + "''{1}''.<br>" 344 + "Please launch the preferences dialog and retrieve another OAuth token." 345 + "</html>", 346 OAuthAccessTokenHolder.getInstance().getAccessTokenKey(), 347 e.getAccessedUrl() == null ? tr("unknown") : e.getAccessedUrl() 348 ); 349 } 350 351 /** 352 * Explains an OSM API exception because of a client timeout (HTTP 408). 353 * 354 * @param e the exception 355 * @return The HTML formatted error message to display 356 */ 357 public static String explainClientTimeout(OsmApiException e) { 358 Logging.error(e); 359 return tr("<html>" 360 + "Communication with the OSM server ''{0}'' timed out. Please retry later." 361 + "</html>", 362 getUrlFromException(e) 363 ); 364 } 365 366 /** 367 * Replies a generic error message for an OSM API exception 368 * 369 * @param e the exception 370 * @return The HTML formatted error message to display 371 */ 372 public static String explainGenericOsmApiException(OsmApiException e) { 373 Logging.error(e); 374 return tr("<html>" 375 + "Communication with the OSM server ''{0}''failed. The server replied<br>" 376 + "the following error code and the following error message:<br>" 377 + "<strong>Error code:<strong> {1}<br>" 378 + "<strong>Error message (untranslated)</strong>: {2}" 379 + "</html>", 380 getUrlFromException(e), 381 e.getResponseCode(), 382 Optional.ofNullable(Optional.ofNullable(e.getErrorHeader()).orElseGet(e::getErrorBody)) 383 .orElse(tr("no error message available")) 384 ); 385 } 386 387 /** 388 * Explains an error due to a 409 conflict 389 * 390 * @param e the exception 391 * @return The HTML formatted error message to display 392 */ 393 public static String explainConflict(OsmApiException e) { 394 Logging.error(e); 395 String msg = e.getErrorHeader(); 396 if (msg != null) { 397 Matcher m = Pattern.compile("The changeset (\\d+) was closed at (.*)").matcher(msg); 398 if (m.matches()) { 399 long changesetId = Long.parseLong(m.group(1)); 400 Instant closeDate = null; 401 try { 402 closeDate = DateUtils.parseInstant(m.group(2)); 403 } catch (UncheckedParseException ex) { 404 Logging.error(tr("Failed to parse date ''{0}'' replied by server.", m.group(2))); 405 Logging.error(ex); 406 } 407 if (closeDate == null) { 408 msg = tr( 409 "<html>Closing of changeset <strong>{0}</strong> failed <br>because it has already been closed.", 410 changesetId 411 ); 412 } else { 413 msg = tr( 414 "<html>Closing of changeset <strong>{0}</strong> failed<br>" 415 +" because it has already been closed on {1}.", 416 changesetId, 417 formatClosedOn(closeDate) 418 ); 419 } 420 return msg; 421 } 422 msg = tr( 423 "<html>The server reported that it has detected a conflict.<br>" + 424 "Error message (untranslated):<br>{0}</html>", 425 msg 426 ); 427 } else { 428 msg = tr( 429 "<html>The server reported that it has detected a conflict."); 430 } 431 return msg.endsWith("</html>") ? msg : (msg + "</html>"); 432 } 433 434 /** 435 * Explains an exception thrown during upload because the changeset which data is 436 * uploaded to is already closed. 437 * 438 * @param e the exception 439 * @return The HTML formatted error message to display 440 */ 441 public static String explainChangesetClosedException(ChangesetClosedException e) { 442 Logging.error(e); 443 return tr( 444 "<html>Failed to upload to changeset <strong>{0}</strong><br>" 445 +"because it has already been closed on {1}.", 446 e.getChangesetId(), 447 e.getClosedOn() == null ? "?" : formatClosedOn(e.getClosedOn()) 448 ); 449 } 450 451 private static String formatClosedOn(Instant closedOn) { 452 return DateUtils.getDateTimeFormatter(FormatStyle.SHORT, FormatStyle.SHORT).format(closedOn.atZone(ZoneId.systemDefault())); 453 } 454 455 /** 456 * Explains an exception with a generic message dialog 457 * 458 * @param e the exception 459 * @return The HTML formatted error message to display 460 */ 461 public static String explainGeneric(Exception e) { 462 String msg = e.getMessage(); 463 if (Utils.isBlank(msg)) { 464 msg = e.toString(); 465 } 466 Logging.error(e); 467 return Utils.escapeReservedCharactersHTML(msg); 468 } 469 470 /** 471 * Explains a {@link SecurityException} which has caused an {@link OsmTransferException}. 472 * This is most likely happening when user tries to access the OSM API from within an 473 * applet which wasn't loaded from the API server. 474 * 475 * @param e the exception 476 * @return The HTML formatted error message to display 477 */ 478 public static String explainSecurityException(OsmTransferException e) { 479 String apiUrl = e.getUrl(); 480 String host = tr("unknown"); 481 try { 482 host = new URL(apiUrl).getHost(); 483 } catch (MalformedURLException ex) { 484 // shouldn't happen 485 Logging.trace(ex); 486 } 487 488 return tr("<html>Failed to open a connection to the remote server<br>" + "''{0}''<br>" 489 + "for security reasons. This is most likely because you are running<br>" 490 + "in an applet and because you did not load your applet from ''{1}''.", apiUrl, host)+"</html>"; 491 } 492 493 /** 494 * Explains a {@link SocketException} which has caused an {@link OsmTransferException}. 495 * This is most likely because there's not connection to the Internet or because 496 * the remote server is not reachable. 497 * 498 * @param e the exception 499 * @return The HTML formatted error message to display 500 */ 501 public static String explainNestedSocketException(OsmTransferException e) { 502 Logging.error(e); 503 return tr("<html>Failed to open a connection to the remote server<br>" + "''{0}''.<br>" 504 + "Please check your internet connection.", e.getUrl())+"</html>"; 505 } 506 507 /** 508 * Explains a {@link IOException} which has caused an {@link OsmTransferException}. 509 * This is most likely happening when the communication with the remote server is 510 * interrupted for any reason. 511 * 512 * @param e the exception 513 * @return The HTML formatted error message to display 514 */ 515 public static String explainNestedIOException(OsmTransferException e) { 516 IOException ioe = getNestedException(e, IOException.class); 517 Logging.error(e); 518 return tr("<html>Failed to upload data to or download data from<br>" + "''{0}''<br>" 519 + "due to a problem with transferring data.<br>" 520 + "Details (untranslated): {1}</html>", 521 e != null ? e.getUrl() : "null", 522 ioe != null ? ioe.getMessage() : "null"); 523 } 524 525 /** 526 * Explains a {@link IllegalDataException} which has caused an {@link OsmTransferException}. 527 * This is most likely happening when JOSM tries to load data in an unsupported format. 528 * 529 * @param e the exception 530 * @return The HTML formatted error message to display 531 */ 532 public static String explainNestedIllegalDataException(OsmTransferException e) { 533 IllegalDataException ide = getNestedException(e, IllegalDataException.class); 534 Logging.error(e); 535 return tr("<html>Failed to download data. " 536 + "Its format is either unsupported, ill-formed, and/or inconsistent.<br>" 537 + "<br>Details (untranslated): {0}</html>", ide != null ? ide.getMessage() : "null"); 538 } 539 540 /** 541 * Explains a {@link OfflineAccessException} which has caused an {@link OsmTransferException}. 542 * This is most likely happening when JOSM tries to access OSM API or JOSM website while in offline mode. 543 * 544 * @param e the exception 545 * @return The HTML formatted error message to display 546 * @since 7434 547 */ 548 public static String explainOfflineAccessException(OsmTransferException e) { 549 OfflineAccessException oae = getNestedException(e, OfflineAccessException.class); 550 Logging.error(e); 551 return tr("<html>Failed to download data.<br>" 552 + "<br>Details: {0}</html>", oae != null ? oae.getMessage() : "null"); 553 } 554 555 /** 556 * Explains a {@link OsmApiException} which was thrown because of an internal server 557 * error in the OSM API server. 558 * 559 * @param e the exception 560 * @return The HTML formatted error message to display 561 */ 562 public static String explainInternalServerError(OsmTransferException e) { 563 Logging.error(e); 564 return tr("<html>The OSM server<br>" + "''{0}''<br>" + "reported an internal server error.<br>" 565 + "This is most likely a temporary problem. Please try again later.", e.getUrl())+"</html>"; 566 } 567 568 /** 569 * Explains a {@link OsmApiException} which was thrown because of a bad request. 570 * 571 * @param e the exception 572 * @return The HTML formatted error message to display 573 */ 574 public static String explainBadRequest(OsmApiException e) { 575 String message = tr("The OSM server ''{0}'' reported a bad request.<br>", getUrlFromException(e)); 576 String errorHeader = e.getErrorHeader(); 577 if (errorHeader != null && (errorHeader.startsWith("The maximum bbox") || 578 errorHeader.startsWith("You requested too many nodes"))) { 579 message += "<br>" 580 + tr("The area you tried to download is too big or your request was too large." 581 + "<br>Either request a smaller area or use an export file provided by the OSM community."); 582 } else if (errorHeader != null) { 583 message += tr("<br>Error message(untranslated): {0}", errorHeader); 584 } 585 Logging.error(e); 586 return "<html>" + message + "</html>"; 587 } 588 589 /** 590 * Explains a {@link OsmApiException} which was thrown because of 591 * bandwidth limit exceeded (HTTP error 509) 592 * 593 * @param e the exception 594 * @return The HTML formatted error message to display 595 */ 596 public static String explainBandwidthLimitExceeded(OsmApiException e) { 597 Logging.error(e); 598 // TODO: Write a proper error message 599 return explainGenericOsmApiException(e); 600 } 601 602 /** 603 * Explains a {@link OsmApiException} which was thrown because a resource wasn't found. 604 * 605 * @param e the exception 606 * @return The HTML formatted error message to display 607 */ 608 public static String explainNotFound(OsmApiException e) { 609 String message = tr("The OSM server ''{0}'' does not know about an object<br>" 610 + "you tried to read, update, or delete. Either the respective object<br>" 611 + "does not exist on the server or you are using an invalid URL to access<br>" 612 + "it. Please carefully check the server''s address ''{0}'' for typos.", 613 getUrlFromException(e)); 614 Logging.error(e); 615 return "<html>" + message + "</html>"; 616 } 617 618 /** 619 * Explains a {@link UnknownHostException} which has caused an {@link OsmTransferException}. 620 * This is most likely happening when there is an error in the API URL or when 621 * local DNS services are not working. 622 * 623 * @param e the exception 624 * @return The HTML formatted error message to display 625 */ 626 public static String explainNestedUnknownHostException(OsmTransferException e) { 627 String apiUrl = e.getUrl(); 628 String host = tr("unknown"); 629 try { 630 host = new URL(apiUrl).getHost(); 631 } catch (MalformedURLException ex) { 632 // shouldn't happen 633 Logging.trace(e); 634 } 635 636 Logging.error(e); 637 return tr("<html>Failed to open a connection to the remote server<br>" + "''{0}''.<br>" 638 + "Host name ''{1}'' could not be resolved. <br>" 639 + "Please check the API URL in your preferences and your internet connection.", apiUrl, host)+"</html>"; 640 } 641 642 /** 643 * Replies the first nested exception of type <code>nestedClass</code> (including 644 * the root exception <code>e</code>) or null, if no such exception is found. 645 * 646 * @param <T> nested exception type 647 * @param e the root exception 648 * @param nestedClass the type of the nested exception 649 * @return the first nested exception of type <code>nestedClass</code> (including 650 * the root exception <code>e</code>) or null, if no such exception is found. 651 * @since 8470 652 */ 653 public static <T> T getNestedException(Exception e, Class<T> nestedClass) { 654 Throwable t = e; 655 while (t != null && !nestedClass.isInstance(t)) { 656 t = t.getCause(); 657 } 658 if (t == null) 659 return null; 660 else if (nestedClass.isInstance(t)) 661 return nestedClass.cast(t); 662 return null; 663 } 664 665 /** 666 * Explains an {@link OsmTransferException} to the user. 667 * 668 * @param e the {@link OsmTransferException} 669 * @return The HTML formatted error message to display 670 */ 671 public static String explainOsmTransferException(OsmTransferException e) { 672 Objects.requireNonNull(e, "e"); 673 if (getNestedException(e, SecurityException.class) != null) 674 return explainSecurityException(e); 675 if (getNestedException(e, SocketException.class) != null) 676 return explainNestedSocketException(e); 677 if (getNestedException(e, UnknownHostException.class) != null) 678 return explainNestedUnknownHostException(e); 679 if (getNestedException(e, IOException.class) != null) 680 return explainNestedIOException(e); 681 if (e instanceof OsmApiInitializationException) 682 return explainOsmApiInitializationException((OsmApiInitializationException) e); 683 684 if (e instanceof ChangesetClosedException) 685 return explainChangesetClosedException((ChangesetClosedException) e); 686 687 if (e instanceof OsmApiException) { 688 OsmApiException oae = (OsmApiException) e; 689 if (oae.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED) 690 return explainPreconditionFailed(oae); 691 if (oae.getResponseCode() == HttpURLConnection.HTTP_GONE) 692 return explainGoneForUnknownPrimitive(oae); 693 if (oae.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR) 694 return explainInternalServerError(oae); 695 if (oae.getResponseCode() == HttpURLConnection.HTTP_BAD_REQUEST) 696 return explainBadRequest(oae); 697 if (oae.getResponseCode() == 509) 698 return explainBandwidthLimitExceeded(oae); 699 } 700 return explainGeneric(e); 701 } 702 703 /** 704 * explains the case of an error due to a delete request on an already deleted 705 * {@link OsmPrimitive}, i.e. a HTTP response code 410, where we don't know which 706 * {@link OsmPrimitive} is causing the error. 707 * 708 * @param e the exception 709 * @return The HTML formatted error message to display 710 */ 711 public static String explainGoneForUnknownPrimitive(OsmApiException e) { 712 return tr( 713 "<html>The server reports that an object is deleted.<br>" 714 + "<strong>Uploading failed</strong> if you tried to update or delete this object.<br> " 715 + "<strong>Downloading failed</strong> if you tried to download this object.<br>" 716 + "<br>" 717 + "The error message is:<br>" + "{0}" 718 + "</html>", Utils.escapeReservedCharactersHTML(e.getMessage())); 719 } 720 721 /** 722 * Explains an {@link Exception} to the user. 723 * 724 * @param e the {@link Exception} 725 * @return The HTML formatted error message to display 726 */ 727 public static String explainException(Exception e) { 728 Logging.error(e); 729 if (e instanceof OsmTransferException) { 730 return explainOsmTransferException((OsmTransferException) e); 731 } else { 732 return explainGeneric(e); 733 } 734 } 735 736 /** 737 * Determines if the OSM API exception has been thrown because user has been blocked or suspended. 738 * @param e OSM API exception 739 * @return {@code true} if the OSM API exception has been thrown because user has been blocked or suspended 740 * @since 15084 741 */ 742 public static boolean isUserBlocked(OsmApiException e) { 743 return OSM_API_BLOCK_MESSAGES.contains(e.getErrorHeader()); 744 } 745 746 static String getUrlFromException(OsmApiException e) { 747 if (e.getAccessedUrl() != null) { 748 try { 749 return new URL(e.getAccessedUrl()).getHost(); 750 } catch (MalformedURLException e1) { 751 Logging.warn(e1); 752 } 753 } 754 if (e.getUrl() != null) { 755 return e.getUrl(); 756 } else { 757 return OsmApi.getOsmApi().getBaseUrl(); 758 } 759 } 760}