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