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