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