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}