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