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>",
513                e != null ? e.getUrl() : "null",
514                ioe != null ? ioe.getMessage() : "null");
515    }
516
517    /**
518     * Explains a {@link IllegalDataException} which has caused an {@link OsmTransferException}.
519     * This is most likely happening when JOSM tries to load data in an unsupported format.
520     *
521     * @param e the exception
522     * @return The HTML formatted error message to display
523     */
524    public static String explainNestedIllegalDataException(OsmTransferException e) {
525        IllegalDataException ide = getNestedException(e, IllegalDataException.class);
526        Main.error(e);
527        return tr("<html>Failed to download data. "
528                + "Its format is either unsupported, ill-formed, and/or inconsistent.<br>"
529                + "<br>Details (untranslated): {0}</html>", ide != null ? ide.getMessage() : "null");
530    }
531
532    /**
533     * Explains a {@link OfflineAccessException} which has caused an {@link OsmTransferException}.
534     * This is most likely happening when JOSM tries to access OSM API or JOSM website while in offline mode.
535     *
536     * @param e the exception
537     * @return The HTML formatted error message to display
538     * @since 7434
539     */
540    public static String explainOfflineAccessException(OsmTransferException e) {
541        OfflineAccessException oae = getNestedException(e, OfflineAccessException.class);
542        Main.error(e);
543        return tr("<html>Failed to download data.<br>"
544                + "<br>Details: {0}</html>", oae != null ? oae.getMessage() : "null");
545    }
546
547    /**
548     * Explains a {@link OsmApiException} which was thrown because of an internal server
549     * error in the OSM API server.
550     *
551     * @param e the exception
552     * @return The HTML formatted error message to display
553     */
554    public static String explainInternalServerError(OsmTransferException e) {
555        Main.error(e);
556        return tr("<html>The OSM server<br>" + "''{0}''<br>" + "reported an internal server error.<br>"
557                + "This is most likely a temporary problem. Please try again later.", e.getUrl())+"</html>";
558    }
559
560    /**
561     * Explains a {@link OsmApiException} which was thrown because of a bad request.
562     *
563     * @param e the exception
564     * @return The HTML formatted error message to display
565     */
566    public static String explainBadRequest(OsmApiException e) {
567        String message = tr("The OSM server ''{0}'' reported a bad request.<br>", getUrlFromException(e));
568        String errorHeader = e.getErrorHeader();
569        if (errorHeader != null && (errorHeader.startsWith("The maximum bbox") ||
570                        errorHeader.startsWith("You requested too many nodes"))) {
571            message += "<br>"
572                + tr("The area you tried to download is too big or your request was too large."
573                        + "<br>Either request a smaller area or use an export file provided by the OSM community.");
574        } else if (errorHeader != null) {
575            message += tr("<br>Error message(untranslated): {0}", errorHeader);
576        }
577        Main.error(e);
578        return "<html>" + message + "</html>";
579    }
580
581    /**
582     * Explains a {@link OsmApiException} which was thrown because of
583     * bandwidth limit exceeded (HTTP error 509)
584     *
585     * @param e the exception
586     * @return The HTML formatted error message to display
587     */
588    public static String explainBandwidthLimitExceeded(OsmApiException e) {
589        Main.error(e);
590        // TODO: Write a proper error message
591        return explainGenericOsmApiException(e);
592    }
593
594    /**
595     * Explains a {@link OsmApiException} which was thrown because a resource wasn't found.
596     *
597     * @param e the exception
598     * @return The HTML formatted error message to display
599     */
600    public static String explainNotFound(OsmApiException e) {
601        String message = tr("The OSM server ''{0}'' does not know about an object<br>"
602                + "you tried to read, update, or delete. Either the respective object<br>"
603                + "does not exist on the server or you are using an invalid URL to access<br>"
604                + "it. Please carefully check the server''s address ''{0}'' for typos.",
605                getUrlFromException(e));
606        Main.error(e);
607        return "<html>" + message + "</html>";
608    }
609
610    /**
611     * Explains a {@link UnknownHostException} which has caused an {@link OsmTransferException}.
612     * This is most likely happening when there is an error in the API URL or when
613     * local DNS services are not working.
614     *
615     * @param e the exception
616     * @return The HTML formatted error message to display
617     */
618    public static String explainNestedUnknownHostException(OsmTransferException e) {
619        String apiUrl = e.getUrl();
620        String host = tr("unknown");
621        try {
622            host = new URL(apiUrl).getHost();
623        } catch (MalformedURLException ex) {
624            // shouldn't happen
625            Main.trace(e);
626        }
627
628        Main.error(e);
629        return tr("<html>Failed to open a connection to the remote server<br>" + "''{0}''.<br>"
630                + "Host name ''{1}'' could not be resolved. <br>"
631                + "Please check the API URL in your preferences and your internet connection.", apiUrl, host)+"</html>";
632    }
633
634    /**
635     * Replies the first nested exception of type <code>nestedClass</code> (including
636     * the root exception <code>e</code>) or null, if no such exception is found.
637     *
638     * @param <T> nested exception type
639     * @param e the root exception
640     * @param nestedClass the type of the nested exception
641     * @return the first nested exception of type <code>nestedClass</code> (including
642     * the root exception <code>e</code>) or null, if no such exception is found.
643     * @since 8470
644     */
645    public static <T> T getNestedException(Exception e, Class<T> nestedClass) {
646        Throwable t = e;
647        while (t != null && !(nestedClass.isInstance(t))) {
648            t = t.getCause();
649        }
650        if (t == null)
651            return null;
652        else if (nestedClass.isInstance(t))
653            return nestedClass.cast(t);
654        return null;
655    }
656
657    /**
658     * Explains an {@link OsmTransferException} to the user.
659     *
660     * @param e the {@link OsmTransferException}
661     * @return The HTML formatted error message to display
662     */
663    public static String explainOsmTransferException(OsmTransferException e) {
664        if (getNestedException(e, SecurityException.class) != null)
665            return explainSecurityException(e);
666        if (getNestedException(e, SocketException.class) != null)
667            return explainNestedSocketException(e);
668        if (getNestedException(e, UnknownHostException.class) != null)
669            return explainNestedUnknownHostException(e);
670        if (getNestedException(e, IOException.class) != null)
671            return explainNestedIOException(e);
672        if (e instanceof OsmApiInitializationException)
673            return explainOsmApiInitializationException((OsmApiInitializationException) e);
674
675        if (e instanceof ChangesetClosedException)
676            return explainChangesetClosedException((ChangesetClosedException) e);
677
678        if (e instanceof OsmApiException) {
679            OsmApiException oae = (OsmApiException) e;
680            if (oae.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED)
681                return explainPreconditionFailed(oae);
682            if (oae.getResponseCode() == HttpURLConnection.HTTP_GONE)
683                return explainGoneForUnknownPrimitive(oae);
684            if (oae.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR)
685                return explainInternalServerError(oae);
686            if (oae.getResponseCode() == HttpURLConnection.HTTP_BAD_REQUEST)
687                return explainBadRequest(oae);
688            if (oae.getResponseCode() == 509)
689                return explainBandwidthLimitExceeded(oae);
690        }
691        return explainGeneric(e);
692    }
693
694    /**
695     * explains the case of an error due to a delete request on an already deleted
696     * {@link OsmPrimitive}, i.e. a HTTP response code 410, where we don't know which
697     * {@link OsmPrimitive} is causing the error.
698     *
699     * @param e the exception
700     * @return The HTML formatted error message to display
701     */
702    public static String explainGoneForUnknownPrimitive(OsmApiException e) {
703        return tr(
704                "<html>The server reports that an object is deleted.<br>"
705                + "<strong>Uploading failed</strong> if you tried to update or delete this object.<br> "
706                + "<strong>Downloading failed</strong> if you tried to download this object.<br>"
707                + "<br>"
708                + "The error message is:<br>" + "{0}"
709                + "</html>", Utils.escapeReservedCharactersHTML(e.getMessage()));
710    }
711
712    /**
713     * Explains an {@link Exception} to the user.
714     *
715     * @param e the {@link Exception}
716     * @return The HTML formatted error message to display
717     */
718    public static String explainException(Exception e) {
719        Main.error(e);
720        if (e instanceof OsmTransferException) {
721            return explainOsmTransferException((OsmTransferException) e);
722        } else {
723            return explainGeneric(e);
724        }
725    }
726
727    static String getUrlFromException(OsmApiException e) {
728        if (e.getAccessedUrl() != null) {
729            try {
730                return new URL(e.getAccessedUrl()).getHost();
731            } catch (MalformedURLException e1) {
732                Main.warn(e1);
733            }
734        }
735        if (e.getUrl() != null) {
736            return e.getUrl();
737        } else {
738            return OsmApi.getOsmApi().getBaseUrl();
739        }
740    }
741}