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.text.SimpleDateFormat;
016import java.util.Collection;
017import java.util.Date;
018import java.util.Locale;
019import java.util.TreeSet;
020import java.util.regex.Matcher;
021import java.util.regex.Pattern;
022
023import org.openstreetmap.josm.Main;
024import org.openstreetmap.josm.data.osm.Node;
025import org.openstreetmap.josm.data.osm.OsmPrimitive;
026import org.openstreetmap.josm.data.osm.Relation;
027import org.openstreetmap.josm.data.osm.Way;
028import org.openstreetmap.josm.gui.preferences.server.OAuthAccessTokenHolder;
029import org.openstreetmap.josm.io.ChangesetClosedException;
030import org.openstreetmap.josm.io.IllegalDataException;
031import org.openstreetmap.josm.io.MissingOAuthAccessTokenException;
032import org.openstreetmap.josm.io.OsmApi;
033import org.openstreetmap.josm.io.OsmApiException;
034import org.openstreetmap.josm.io.OsmApiInitializationException;
035import org.openstreetmap.josm.io.OsmTransferException;
036import org.openstreetmap.josm.io.auth.CredentialsManager;
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     */
050    public static String explainOsmApiInitializationException(OsmApiInitializationException e) {
051        e.printStackTrace();
052        String msg = 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.", Main.pref.get(
055                        "osm-server.url", OsmApi.DEFAULT_API_URL));
056        return msg;
057    }
058
059
060    /**
061     *  Creates the error message
062     *
063     * @param e the exception
064     */
065    public static String explainMissingOAuthAccessTokenException(MissingOAuthAccessTokenException e) {
066        e.printStackTrace();
067        String msg = tr(
068                "<html>Failed to authenticate at the OSM server ''{0}''.<br>"
069                + "You are using OAuth to authenticate but currently there is no<br>"
070                + "OAuth Access Token configured.<br>"
071                + "Please open the Preferences Dialog and generate or enter an Access Token."
072                + "</html>",
073                Main.pref.get("osm-server.url", OsmApi.DEFAULT_API_URL)
074        );
075        return msg;
076    }
077
078    public static Pair<OsmPrimitive, Collection<OsmPrimitive>> parsePreconditionFailed(String msg) {
079        final String ids = "(\\d+(?:,\\d+)*)";
080        final Collection<OsmPrimitive> refs = new TreeSet<OsmPrimitive>(); // error message can contain several times the same way
081        Matcher m;
082        m = Pattern.compile(".*Node (\\d+) is still used by relations " + ids + ".*").matcher(msg);
083        if (m.matches()) {
084            OsmPrimitive n = new Node(Long.parseLong(m.group(1)));
085            for (String s : m.group(2).split(",")) {
086                refs.add(new Relation(Long.parseLong(s)));
087            }
088            return Pair.create(n, refs);
089        }
090        m = Pattern.compile(".*Node (\\d+) is still used by ways " + ids + ".*").matcher(msg);
091        if (m.matches()) {
092            OsmPrimitive n = new Node(Long.parseLong(m.group(1)));
093            for (String s : m.group(2).split(",")) {
094                refs.add(new Way(Long.parseLong(s)));
095            }
096            return Pair.create(n, refs);
097        }
098        m = Pattern.compile(".*The relation (\\d+) is used in relations? " + ids + ".*").matcher(msg);
099        if (m.matches()) {
100            OsmPrimitive n = new Relation(Long.parseLong(m.group(1)));
101            for (String s : m.group(2).split(",")) {
102                refs.add(new Relation(Long.parseLong(s)));
103            }
104            return Pair.create(n, refs);
105        }
106        m = Pattern.compile(".*Way (\\d+) is still used by relations " + ids + ".*").matcher(msg);
107        if (m.matches()) {
108            OsmPrimitive n = new Way(Long.parseLong(m.group(1)));
109            for (String s : m.group(2).split(",")) {
110                refs.add(new Relation(Long.parseLong(s)));
111            }
112            return Pair.create(n, refs);
113        }
114        m = Pattern.compile(".*Way (\\d+) requires the nodes with id in " + ids + ".*").matcher(msg); // ... ", which either do not exist, or are not visible"
115        if (m.matches()) {
116            OsmPrimitive n = new Way(Long.parseLong(m.group(1)));
117            for (String s : m.group(2).split(",")) {
118                refs.add(new Node(Long.parseLong(s)));
119            }
120            return Pair.create(n, refs);
121        }
122        return null;
123    }
124
125    /**
126     * Explains an upload error due to a violated precondition, i.e. a HTTP return code 412
127     *
128     * @param e the exception
129     */
130    public static String explainPreconditionFailed(OsmApiException e) {
131        e.printStackTrace();
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        String msg = null;
273        if (header != null) {
274            if (body != null && !header.equals(body)) {
275                msg = header + " (" + body + ")";
276            } else {
277                msg = header;
278            }
279        } else {
280            msg = body;
281        }
282
283        if (msg != null && !msg.isEmpty()) {
284            return tr("<html>"
285                    + "Authorisation at the OSM server failed.<br>"
286                    + "The server reported the following error:<br>"
287                    + "''{0}''"
288                    + "</html>",
289                    msg
290            );
291        } else {
292            return tr("<html>"
293                    + "Authorisation at the OSM server failed.<br>"
294                    + "</html>"
295            );
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                    Main.error(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().isEmpty()) {
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>"
491                + "Details (untranslated): {1}</html>", apiUrl, ioe
492                .getMessage());
493        e.printStackTrace();
494        return message;
495    }
496
497    /**
498     * Explains a {@link IllegalDataException} which has caused an {@link OsmTransferException}.
499     * This is most likely happening when JOSM tries to load data in in an unsupported format.
500     *
501     * @param e the exception
502     */
503    public static String explainNestedIllegalDataException(OsmTransferException e) {
504        IllegalDataException ide = getNestedException(e, IllegalDataException.class);
505        String message = tr("<html>Failed to download data. "
506                + "Its format is either unsupported, ill-formed, and/or inconsistent.<br>"
507                + "<br>Details (untranslated): {0}</html>", ide.getMessage());
508        e.printStackTrace();
509        return message;
510    }
511
512    /**
513     * Explains a {@link OsmApiException} which was thrown because of an internal server
514     * error in the OSM API server..
515     *
516     * @param e the exception
517     */
518
519    public static String explainInternalServerError(OsmTransferException e) {
520        String apiUrl = e.getUrl();
521        String message = tr("<html>The OSM server<br>" + "''{0}''<br>" + "reported an internal server error.<br>"
522                + "This is most likely a temporary problem. Please try again later.", apiUrl);
523        e.printStackTrace();
524        return message;
525    }
526
527    /**
528     * Explains a {@link OsmApiException} which was thrown because of a bad
529     * request
530     *
531     * @param e the exception
532     */
533    public static String explainBadRequest(OsmApiException e) {
534        String apiUrl = OsmApi.getOsmApi().getBaseUrl();
535        String message = tr("The OSM server ''{0}'' reported a bad request.<br>", apiUrl);
536        if (e.getErrorHeader() != null &&
537                (e.getErrorHeader().startsWith("The maximum bbox") ||
538                        e.getErrorHeader().startsWith("You requested too many nodes"))) {
539            message += "<br>"
540                + tr("The area you tried to download is too big or your request was too large."
541                        + "<br>Either request a smaller area or use an export file provided by the OSM community.");
542        } else if (e.getErrorHeader() != null) {
543            message += tr("<br>Error message(untranslated): {0}", e.getErrorHeader());
544        }
545        message = "<html>" + message + "</html>";
546        e.printStackTrace();
547        return message;
548    }
549
550    /**
551     * Explains a {@link OsmApiException} which was thrown because of
552     * bandwidth limit exceeded (HTTP error 509)
553     *
554     * @param e the exception
555     */
556    public static String explainBandwidthLimitExceeded(OsmApiException e) {
557        // TODO: Write a proper error message
558        String message = explainGenericOsmApiException(e);
559        e.printStackTrace();
560        return message;
561    }
562
563
564    /**
565     * Explains a {@link OsmApiException} which was thrown because a resource wasn't found.
566     *
567     * @param e the exception
568     */
569    public static String explainNotFound(OsmApiException e) {
570        String apiUrl = OsmApi.getOsmApi().getBaseUrl();
571        String message = tr("The OSM server ''{0}'' does not know about an object<br>"
572                + "you tried to read, update, or delete. Either the respective object<br>"
573                + "does not exist on the server or you are using an invalid URL to access<br>"
574                + "it. Please carefully check the server''s address ''{0}'' for typos."
575                , apiUrl);
576        message = "<html>" + message + "</html>";
577        e.printStackTrace();
578        return message;
579    }
580
581    /**
582     * Explains a {@link UnknownHostException} which has caused an {@link OsmTransferException}.
583     * This is most likely happening when there is an error in the API URL or when
584     * local DNS services are not working.
585     *
586     * @param e the exception
587     */
588
589    public static String explainNestedUnknownHostException(OsmTransferException e) {
590        String apiUrl = e.getUrl();
591        String host = tr("unknown");
592        try {
593            host = new URL(apiUrl).getHost();
594        } catch (MalformedURLException ex) {
595            // shouldn't happen
596        }
597
598        String message = tr("<html>Failed to open a connection to the remote server<br>" + "''{0}''.<br>"
599                + "Host name ''{1}'' could not be resolved. <br>"
600                + "Please check the API URL in your preferences and your internet connection.", apiUrl, host);
601        e.printStackTrace();
602        return message;
603    }
604
605    /**
606     * Replies the first nested exception of type <code>nestedClass</code> (including
607     * the root exception <code>e</code>) or null, if no such exception is found.
608     *
609     * @param <T>
610     * @param e the root exception
611     * @param nestedClass the type of the nested exception
612     * @return the first nested exception of type <code>nestedClass</code> (including
613     * the root exception <code>e</code>) or null, if no such exception is found.
614     */
615    protected static <T> T getNestedException(Exception e, Class<T> nestedClass) {
616        Throwable t = e;
617        while (t != null && !(nestedClass.isInstance(t))) {
618            t = t.getCause();
619        }
620        if (t == null)
621            return null;
622        else if (nestedClass.isInstance(t))
623            return nestedClass.cast(t);
624        return null;
625    }
626
627    /**
628     * Explains an {@link OsmTransferException} to the user.
629     *
630     * @param e the {@link OsmTransferException}
631     */
632    public static String explainOsmTransferException(OsmTransferException e) {
633        if (getNestedException(e, SecurityException.class) != null)
634            return explainSecurityException(e);
635        if (getNestedException(e, SocketException.class) != null)
636            return explainNestedSocketException(e);
637        if (getNestedException(e, UnknownHostException.class) != null)
638            return explainNestedUnknownHostException(e);
639        if (getNestedException(e, IOException.class) != null)
640            return explainNestedIOException(e);
641        if (e instanceof OsmApiInitializationException)
642            return explainOsmApiInitializationException((OsmApiInitializationException) e);
643
644        if (e instanceof ChangesetClosedException)
645            return explainChangesetClosedException((ChangesetClosedException)e);
646
647        if (e instanceof OsmApiException) {
648            OsmApiException oae = (OsmApiException) e;
649            if (oae.getResponseCode() == HttpURLConnection.HTTP_PRECON_FAILED)
650                return explainPreconditionFailed(oae);
651            if (oae.getResponseCode() == HttpURLConnection.HTTP_GONE)
652                return explainGoneForUnknownPrimitive(oae);
653            if (oae.getResponseCode() == HttpURLConnection.HTTP_INTERNAL_ERROR)
654                return explainInternalServerError(oae);
655            if (oae.getResponseCode() == HttpURLConnection.HTTP_BAD_REQUEST)
656                return explainBadRequest(oae);
657            if (oae.getResponseCode() == 509)
658                return explainBandwidthLimitExceeded(oae);
659        }
660        return explainGeneric(e);
661    }
662
663    /**
664     * explains the case of an error due to a delete request on an already deleted
665     * {@link OsmPrimitive}, i.e. a HTTP response code 410, where we don't know which
666     * {@link OsmPrimitive} is causing the error.
667     *
668     * @param e the exception
669     */
670    public static String explainGoneForUnknownPrimitive(OsmApiException e) {
671        String msg = tr(
672                "<html>The server reports that an object is deleted.<br>"
673                + "<strong>Uploading failed</strong> if you tried to update or delete this object.<br> "
674                + "<strong>Downloading failed</strong> if you tried to download this object.<br>"
675                + "<br>"
676                + "The error message is:<br>" + "{0}"
677                + "</html>", escapeReservedCharactersHTML(e.getMessage()));
678        return msg;
679
680    }
681
682    /**
683     * Explains an {@link Exception} to the user.
684     *
685     * @param e the {@link Exception}
686     */
687    public static String explainException(Exception e) {
688        String msg = "";
689        if (e instanceof OsmTransferException) {
690            msg = explainOsmTransferException((OsmTransferException) e);
691        } else {
692            msg = explainGeneric(e);
693        }
694        e.printStackTrace();
695        return msg;
696    }
697
698    /**
699     * Replaces some HTML reserved characters (<, > and &) by their equivalent entity (&lt;, &gt; and &amp;);
700     * @param s The unescaped string
701     * @return The escaped string
702     */
703    public static String escapeReservedCharactersHTML(String s) {
704        return s == null ? "" : s.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
705    }
706}