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