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}