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