You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2024/01/27 16:51:27 UTC

(sis) branch geoapi-4.0 updated: Reduce the verbosity of log record or error message during XML unmarshalling. - Some log records were repeated many times. - JAXBException with very long messages had the message repeated in their causes.

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new fce18cb382 Reduce the verbosity of log record or error message during XML unmarshalling. - Some log records were repeated many times. - JAXBException with very long messages had the message repeated in their causes.
fce18cb382 is described below

commit fce18cb3826104e0094a1daf79463baabf442ccd
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jan 27 17:49:58 2024 +0100

    Reduce the verbosity of log record or error message during XML unmarshalling.
    - Some log records were repeated many times.
    - JAXBException with very long messages had the message repeated in their causes.
---
 .../main/org/apache/sis/xml/ReferenceResolver.java |  74 ++++++----
 .../main/org/apache/sis/xml/bind/Context.java      |  47 +++++--
 .../apache/sis/xml/util/ExceptionSimplifier.java   | 150 +++++++++++++++++++++
 .../apache/sis/xml/util/ExternalLinkHandler.java   |   8 +-
 .../main/org/apache/sis/io/wkt/Element.java        |   3 +-
 .../apache/sis/referencing/internal/Resources.java |   5 +
 .../sis/referencing/internal/Resources.properties  |   1 +
 .../referencing/internal/Resources_fr.properties   |   1 +
 .../sis/referencing/util/ReferencingUtilities.java |   5 +-
 .../org/apache/sis/storage/base/PRJDataStore.java  |   8 +-
 .../org/apache/sis/storage/base/URIDataStore.java  |  19 ++-
 .../main/org/apache/sis/util/Exceptions.java       |  13 +-
 .../main/org/apache/sis/util/resources/Errors.java |  15 ++-
 .../apache/sis/util/resources/Errors.properties    |   3 +-
 .../apache/sis/util/resources/Errors_fr.properties |   3 +-
 .../resources/ResourceInternationalString.java     |   4 +-
 16 files changed, 292 insertions(+), 67 deletions(-)

diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
index 8d5828e801..68ef4e7bfb 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
@@ -157,6 +157,7 @@ public class ReferenceResolver {
      * </ul>
      *
      * If an object is found but is not of the class declared in {@code type},
+     * or if an {@link Exception} was thrown during object unmarshalling,
      * then this method emits a warning and returns {@code null}.
      *
      * @param  <T>      the compile-time type of the {@code type} argument.
@@ -173,6 +174,7 @@ public class ReferenceResolver {
             return null;
         }
         final Object object;
+        final short reasonIfNull;             // An Errors.Key value with one parameter, or 0.
         final Context c = (context instanceof Context) ? (Context) context : Context.current();
         if (!href.isAbsolute() && Strings.isNullOrEmpty(href.getPath())) {
             /*
@@ -187,10 +189,12 @@ public class ReferenceResolver {
                 return null;
             }
             object = Context.getObjectForID(c, fragment);
+            reasonIfNull = Errors.Keys.NotABackwardReference_1;
         } else try {
             /*
-             * URI to an external document. We let `ExternalLinkHandler` decide how to replace relative URI
-             * by absolute URI. It may depend on whether user has specified a `javax.xml.stream.XMLResolver`
+             * URI to an external document. If a `javax.xml.stream.XMLResolver` property was set on the unmarshaller,
+             * use the user-supplied `URIResolver`. If there is no URI resolver or the URI resolver can not resolve,
+             * fallback on the Apache SIS `ExternalLinkHandler` implementation. The latter is the usual case.
              */
             final ExternalLinkHandler handler = Context.linkHandler(c);
             Source source = null;
@@ -200,32 +204,38 @@ public class ReferenceResolver {
                     source = externalSourceResolver.resolve(href.toString(), base.toString());
                 }
             }
-            if (source == null) {
-                source = handler.openReader(href);
+            if (source == null && (source = handler.openReader(href)) == null) {
+                reasonIfNull = Errors.Keys.CanNotResolveAsAbsolutePath_1;
+                object = null;
+            } else {
+                object = resolveExternal(context, source);
+                reasonIfNull = 0;
             }
-            object = (source != null) ? resolveExternal(context, source) : null;
         } catch (Exception e) {
             ExternalLinkHandler.warningOccured(href, e);
             return null;
         }
         /*
-         * At this point, the referenced object has been fetched.
-         * Verify its validity.
+         * At this point, the referenced object has been fetched or unmarshalled.
+         * The result may be null, in which case the warning to emit depends on the
+         * reason why the object is null: could not resolve, or could not unmarshall.
          */
         if (type.isInstance(object)) {
             return type.cast(object);
-        } else {
-            final short key;
-            final Object[] args;
-            if (object == null) {
-                key = Errors.Keys.NotABackwardReference_1;
-                args = new Object[] {href.toString()};
-            } else {
-                key = Errors.Keys.UnexpectedTypeForReference_3;
-                args = new Object[] {href.toString(), type, object.getClass()};
+        }
+        final short key;
+        final Object[] args;
+        if (object == null) {
+            if (reasonIfNull == 0) {
+                return null;
             }
-            Context.warningOccured(c, ReferenceResolver.class, "resolve", Errors.class, key, args);
+            key = reasonIfNull;
+            args = new Object[] {href.toString()};
+        } else {
+            key = Errors.Keys.UnexpectedTypeForReference_3;
+            args = new Object[] {href.toString(), type, object.getClass()};
         }
+        Context.warningOccured(c, ReferenceResolver.class, "resolve", Errors.class, key, args);
         return null;
     }
 
@@ -245,9 +255,18 @@ public class ReferenceResolver {
      * </ul>
      * The resolved URL, if known, should be available in {@link Source#getSystemId()}.
      *
+     * <h4>Error handling</h4>
+     * The default implementation keeps a cache during the execution of an {@code XML.unmarshall(…)} method
+     * (or actually, during a {@linkplain MarshallerPool pooled unmarshaller} method).
+     * If an exception is thrown during the document unmarshalling, this failure is also recorded in the cache.
+     * Therefor, the exception is thrown only during the first attempt to read the document
+     * and {@code null} is returned directly on next attempts for the same source.
+     * Exceptions thrown by this method are caught by {@link #resolve(MarshalContext, Class, XLink) resolve(…)}
+     * and reported as warnings.
+     *
      * @param  context  context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
      * @param  source   source of the document specified by the {@code xlink:href} attribute value.
-     * @return an object for the given source, or {@code null} if none.
+     * @return an object for the given source, or {@code null} if none, for example because of failure in a previous attempt.
      * @throws Exception if an error occurred while opening or parsing the document.
      *
      * @since 1.5
@@ -278,12 +297,12 @@ public class ReferenceResolver {
          * and the URI fragment to use as a GML identifier. Check if the document is in the cache.
          * Note that if the fragment is null, then by convention we lookup for the whole document.
          */
-        final Context c = Context.current();
+        final Context c = (context instanceof Context) ? (Context) context : Context.current();
         if (c != null) {
             final Object object = c.getExternalObjectForID(document, fragment);
             if (object != null) {
                 XmlUtilities.close(source);
-                return object;
+                return (object != Context.INVALID_OBJECT) ? object : null;
             }
         }
         /*
@@ -297,10 +316,17 @@ public class ReferenceResolver {
             ((Pooled) m).forIncludedDocument(document);
         }
         final Object object;
-        if (uri != null) {
-            object = m.unmarshal(uri.toURL());
-        } else {
-            object = m.unmarshal(source);
+        try {
+            if (uri != null) {
+                object = m.unmarshal(uri.toURL());
+            } else {
+                object = m.unmarshal(source);
+            }
+        } catch (Exception e) {
+            if (c != null) {
+                c.cacheDocument(document, Context.INVALID_OBJECT);
+            }
+            throw e;
         }
         pool.recycle(m);
         /*
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/Context.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/Context.java
index 6558346f42..e66c25496d 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/Context.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/Context.java
@@ -117,8 +117,19 @@ public final class Context extends MarshalContext {
      */
     private static final ThreadLocal<Context> CURRENT = new ThreadLocal<>();
 
+    /**
+     * A sentinel value meaning that unmarshalling of a document was already attempted before and failed.
+     * This is used for documents referenced from a larger document using {@code xlink:href}.
+     *
+     * @see #getExternalObjectForID(Object, String)
+     * @see #cacheDocument(Object, Object)
+     */
+    public static final Object INVALID_OBJECT = Void.TYPE;
+
     /**
      * The logger to use for warnings that are specific to XML.
+     *
+     * @see #warningOccured(Context, Level, Class, String, Throwable, Class, short, Object...)
      */
     public static final Logger LOGGER = Logger.getLogger(Loggers.XML);
 
@@ -180,6 +191,10 @@ public final class Context extends MarshalContext {
      * At marhalling time, this map is used for avoiding duplicated identifiers in the same XML document.
      * At unmarshalling time, this is used for getting a previously unmarshalled object from its identifier.
      *
+     * <p>By convention, the {@code null} key is associated to the whole document. This convention is used if
+     * the document being unmarshalled is part of a larger document and was referenced by {@code xlink:href}.
+     * In such case, this map is also a value of the {@link #documentToXmlids} map.</p>
+     *
      * @see #getObjectForID(Context, String)
      */
     private final Map<String,Object> xmlidToObject;
@@ -202,8 +217,10 @@ public final class Context extends MarshalContext {
      * from a file or an URL, {@code systemId} should be the value of {@link java.net.URI#toASCIIString()} for
      * consistency with {@link javax.xml.transform.stream.StreamSource}.  However, URI instances are preferred
      * in this map because the {@link URI#equals(Object)} method applies some rules regarding case-sensitivity
-     * that {@link String#equals(Object)} cannot know. Values of the map are the {@link #xmlidToObject} maps of
-     * the corresponding document. By convention, the object associated to the null key is the whole document.</p>
+     * that {@link String#equals(Object)} cannot know.</p>
+     *
+     * <p>Values of this map are the {@link #xmlidToObject} maps of the corresponding document.
+     * See {@link #xmlidToObject} for a description of the meaning of those maps.</p>
      */
     private final Map<Object, Map<String,Object>> documentToXmlids;
 
@@ -219,6 +236,8 @@ public final class Context extends MarshalContext {
 
     /**
      * The object to inform about warnings, or {@code null} if none.
+     *
+     * @see org.apache.sis.xml.XML#WARNING_FILTER
      */
     private final Filter logFilter;
 
@@ -679,12 +698,20 @@ public final class Context extends MarshalContext {
      * By convention, a null {@code id} returns the whole document.</p>
      *
      * @param  systemId  document identifier (without the fragment part) as an {@link URI} or a {@link String}.
-     * @param  id        the fragment part of the URI identifying the object to get.
-     * @return the object associated to the given identifier, or {@code null} if none.
+     * @param  fragment  the fragment part of the URI, or {@code null} for the whole document.
+     * @return the object associated to the given identifier, or {@code null} if none,
+     *         or {@link #INVALID_OBJECT} if a parsing was previously attempted and failed.
      */
-    public final Object getExternalObjectForID(final Object systemId, final String id) {
+    public final Object getExternalObjectForID(final Object systemId, final String fragment) {
         final Map<String,Object> cache = documentToXmlids.get(systemId);
-        return (cache != null) ? cache.get(id) : null;
+        if (cache == null) {
+            return null;
+        }
+        final Object value = cache.get(fragment);
+        if (value == null && cache.get(null) == INVALID_OBJECT) {
+            return INVALID_OBJECT;
+        }
+        return value;
     }
 
     /**
@@ -693,7 +720,7 @@ public final class Context extends MarshalContext {
      * The fragment part is obtained by {@link #getExternalObjectForID(Object, String)}.
      *
      * @param  systemId  document identifier (without the fragment part) as an {@link URI} or a {@link String}.
-     * @param  document  the document to cache.
+     * @param  document  the document to cache, or {@link #INVALID_OBJECT} for recording a failure to read the document.
      */
     public final void cacheDocument(final Object systemId, final Object document) {
         final Map<String, Object> cache = documentToXmlids.get(systemId);
@@ -766,7 +793,7 @@ public final class Context extends MarshalContext {
 
     /**
      * Sends a warning to the warning listener if there is one, or logs the warning otherwise.
-     * In the latter case, this method logs to the given logger.
+     * In the latter case, this method logs to {@link #LOGGER}.
      *
      * <p>If the given {@code resources} is {@code null}, then this method will build the log
      * message from the {@code exception}.</p>
@@ -776,7 +803,7 @@ public final class Context extends MarshalContext {
      * @param  classe     the class to declare as the warning source.
      * @param  method     the name of the method to declare as the warning source.
      * @param  exception  the exception thrown, or {@code null} if none.
-     * @param  resources  either {@code Errors.class}, {@code Messages.class} or {@code null} for the exception message.
+     * @param  resources  either {@code Errors.class} or {@code Messages.class}, or {@code null} for the exception message.
      * @param  key        the resource keys as one of the constants defined in the {@code Keys} inner class.
      * @param  arguments  the arguments to be given to {@code MessageFormat} for formatting the log message.
      */
@@ -837,7 +864,7 @@ public final class Context extends MarshalContext {
 
     /**
      * Convenience method for sending a warning for the given exception.
-     * The logger will be {@code "org.apache.sis.xml"}.
+     * The log message will be the message of the given exception.
      *
      * @param  context    the current context, or {@code null} if none.
      * @param  classe     the class to declare as the warning source.
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExceptionSimplifier.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExceptionSimplifier.java
new file mode 100644
index 0000000000..a7cac1b5b1
--- /dev/null
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExceptionSimplifier.java
@@ -0,0 +1,150 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.xml.util;
+
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import org.xml.sax.SAXParseException;
+import org.apache.sis.system.Loggers;
+import org.apache.sis.xml.bind.Context;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * Simplifies an exception before to log it. This class reduces log flooding when an exception has a very long message
+ * (e.g. JAXB enumerating all elements that it was expecting) and that long message is repeated in the exception cause.
+ * This class also handles the identification of the location where the error occurred.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class ExceptionSimplifier {
+    /**
+     * The key to use for producing an error message, or 0 for using the exception message.
+     * Shall be a constant from {@link Errors.Keys}.
+     */
+    private short errorKey;
+
+    /**
+     * The values to insert in the error message, or {@code null} for using the exception message.
+     */
+    private Object[] errorValues;
+
+    /**
+     * The exception.
+     */
+    public final Exception exception;
+
+    /**
+     * Simplifies the given exception if possible, and produces an error message.
+     *
+     * @param source     the source (URI or Path) that couldn't be parsed, or {@code null} if unknown.
+     * @param exception  the error that occurred while parsing the source.
+     */
+    public ExceptionSimplifier(Object source, Exception exception) {
+        int line = -1, column = -1;
+        for (Throwable cause = exception; cause != null; cause = cause.getCause()) {
+            if (cause instanceof SAXParseException) {
+                var s = (SAXParseException) cause;
+                if ((line | column) < 0) {
+                    line = s.getLineNumber();
+                    column = s.getColumnNumber();
+                }
+                if (source == null) {
+                    source = s.getPublicId();
+                    if (source == null) {
+                        source = s.getSystemId();
+                    }
+                }
+            }
+            if (cause != exception) {
+                final String msg = exception.getMessage();
+                if (msg != null) {
+                    final String s = cause.getMessage();
+                    if (s != null && !msg.contains(s)) break;
+                }
+                if (exception.getClass().isInstance(cause)) {
+                    exception = (Exception) cause;
+                }
+            }
+        }
+        this.exception = exception;
+        if (source == null) {
+            final ExternalLinkHandler handler = Context.linkHandler(Context.current());
+            if (handler != null) {
+                source = handler.getBase();
+            }
+        }
+        if (source != null) {
+            if ((line | column) < 0) {
+                errorKey = Errors.Keys.CanNotRead_1;
+                errorValues = new Object[] {source};
+            } else {
+                errorKey = Errors.Keys.CanNotRead_3;
+                errorValues = new Object[] {source, line, column};
+            }
+        }
+    }
+
+    /**
+     * Returns the error message.
+     *
+     * @param  locale  desired locale for the error message.
+     * @return the error message, or {@code null} if none.
+     */
+    public String getMessage(final Locale locale) {
+        if (errorKey != 0) {
+            return Errors.getResources(locale).getString(errorKey, errorValues);
+        } else {
+            return exception.getMessage();
+        }
+    }
+
+    /**
+     * Sends the exception to the warning listener if there is one, or logs the warning otherwise.
+     * In the latter case, this method logs to {@link Context#LOGGER}.
+     *
+     * @param  context  the current context, or {@code null} if none.
+     * @param  classe   the class to declare as the warning source.
+     * @param  method   the name of the method to declare as the warning source.
+     */
+    public void report(final Context context, final Class<?> classe, final String method) {
+        Context.warningOccured(context, Level.WARNING, classe, method, exception,
+                (errorKey != 0) ? Errors.class : null, errorKey, errorValues);
+    }
+
+    /**
+     * Creates a log record for the warning.
+     *
+     * @param  classe  the class to declare as the warning source.
+     * @param  method  the name of the method to declare as the warning source.
+     * @return the log record.
+     */
+    public LogRecord record(final Class<?> classe, final String method) {
+        final LogRecord record;
+        if (errorKey != 0) {
+            record = Errors.getResources((Locale) null).getLogRecord(Level.WARNING, errorKey, errorValues);
+        } else {
+            record = new LogRecord(Level.WARNING, exception.getMessage());
+        }
+        record.setLoggerName(Loggers.XML);
+        record.setSourceClassName(classe.getCanonicalName());
+        record.setSourceMethodName(method);
+        record.setThrown(exception);
+        return record;
+    }
+}
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java
index 9dbbdc9a22..06fdeddf4d 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java
@@ -21,7 +21,6 @@ import java.io.InputStream;
 import java.net.URL;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.util.logging.Level;
 import javax.xml.stream.Location;
 import javax.xml.stream.XMLResolver;
 import javax.xml.stream.XMLInputFactory;
@@ -181,12 +180,11 @@ public class ExternalLinkHandler {
      * The latter assumption is valid if {@code ReferenceResolver.resolve(…)} is the only
      * code invoking, directly or indirectly, this {@code warning(…)} method.
      *
-     * @param  href   the URI that cannot be parsed.
-     * @param  cause  the exception that occurred while trying to process the document.
+     * @param  href  the URI that cannot be parsed.
+     * @param  cause the exception that occurred while trying to process the document.
      */
     public static void warningOccured(final Object href, final Exception cause) {
-        Context.warningOccured(Context.current(), Level.WARNING, ReferenceResolver.class, "resolve",
-                               cause, Errors.class, Errors.Keys.CanNotRead_1, href);
+        new ExceptionSimplifier(href, cause).report(Context.current(), ReferenceResolver.class, "resolve");
     }
 
     /**
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
index 8b0f5bd021..425f7808bb 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Element.java
@@ -31,6 +31,7 @@ import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.referencing.util.WKTKeywords;
+import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.util.internal.CollectionsExt;
 import static org.apache.sis.util.CharSequences.skipLeadingWhitespaces;
 
@@ -404,7 +405,7 @@ final class Element {
      * @return the exception to be thrown.
      */
     final ParseException parseFailed(final Exception cause) {
-        return new UnparsableObjectException(errorLocale, Errors.Keys.ErrorIn_2,
+        return new UnparsableObjectException(errorLocale, Resources.Keys.CannotParseElement_2,
                 new String[] {keyword, Exceptions.getLocalizedMessage(cause, errorLocale)}, offset).initCause(cause);
     }
 
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java
index df0be0c83f..ac36d0818d 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.java
@@ -167,6 +167,11 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short CanNotUseGeodeticParameters_2 = 9;
 
+        /**
+         * Cannot parse the “{0}” element: {1}
+         */
+        public static final short CannotParseElement_2 = 101;
+
         /**
          * Axis directions {0} and {1} are colinear.
          */
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties
index 18b3aba29d..1ceec59dae 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources.properties
@@ -22,6 +22,7 @@
 # Information messages or non-fatal warnings
 #
 AmbiguousEllipsoid_1              = Ambiguity between inverse flattening and semi minor axis length for \u201c{0}\u201d. Using inverse flattening.
+CannotParseElement_2              = Cannot parse the \u201c{0}\u201d element: {1}
 ConformanceMeansDatumShift        = This result indicates if a datum shift method has been applied.
 ConstantProjParameterValue_1      = This parameter is shown for completeness, but should never have a value different than {0} for this projection.
 DeprecatedCode_3                  = Code \u201c{0}\u201d is deprecated and replaced by code {1}. Reason is: {2}
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties
index 98e953dc70..27a9408e59 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/internal/Resources_fr.properties
@@ -27,6 +27,7 @@
 # Information messages or non-fatal warnings
 #
 AmbiguousEllipsoid_1              = Ambigu\u00eft\u00e9 entre l\u2019aplatissement et la longueur du semi-axe mineur pour \u00ab\u202f{0}\u202f\u00bb. Utilise l\u2019aplatissement.
+CannotParseElement_2              = Ne peut pas d\u00e9coder l\u2019\u00e9l\u00e9ment \u00ab\u202f{0}\u202f\u00bb\u00a0: {1}
 ConformanceMeansDatumShift        = Ce r\u00e9sultat indique si un changement de r\u00e9f\u00e9rentiel a \u00e9t\u00e9 appliqu\u00e9.
 ConstantProjParameterValue_1      = Ce param\u00e8tre est montr\u00e9 pour \u00eatre plus complet, mais sa valeur ne devrait jamais \u00eatre diff\u00e9rente de {0} pour cette projection.
 DeprecatedCode_3                  = Le code \u00ab\u202f{0}\u202f\u00bb est d\u00e9pr\u00e9ci\u00e9 et remplac\u00e9 par le code {1}. La raison est\u00a0: {2}
diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/ReferencingUtilities.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/ReferencingUtilities.java
index d0bb28fc88..4234653bd1 100644
--- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/ReferencingUtilities.java
+++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/util/ReferencingUtilities.java
@@ -19,6 +19,7 @@ package org.apache.sis.referencing.util;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Collection;
+import java.util.NoSuchElementException;
 import javax.measure.Unit;
 import javax.measure.quantity.Angle;
 import org.opengis.annotation.UML;
@@ -220,6 +221,7 @@ public final class ReferencingUtilities extends Static {
      * @param  addTo   where to add the single CRS in order to obtain a flat view of {@code source}.
      * @return {@code true} if this method found only single CRS in {@code source}, in which case {@code addTo}
      *         got the same content (assuming that {@code addTo} was empty prior this method call).
+     * @throws NoSuchElementException if a CRS component is missing.
      * @throws ClassCastException if a CRS is neither a {@link SingleCRS} or a {@link CompoundCRS}.
      *
      * @see org.apache.sis.referencing.CRS#getSingleComponents(CoordinateReferenceSystem)
@@ -243,10 +245,11 @@ public final class ReferencingUtilities extends Static {
                 final String message;
                 if (candidate instanceof NilObject) {
                     message = Errors.format(Errors.Keys.NilObject_1, Identifiers.getNilReason((NilObject) candidate));
+                    throw new NoSuchElementException(message);
                 } else {
                     message = Errors.format(Errors.Keys.NestedElementNotAllowed_1, getInterface(candidate));
+                    throw new ClassCastException(message);
                 }
-                throw new ClassCastException(message);
             }
         }
         return sameContent;
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
index dbfeba3ab8..7dcbe0f4c9 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
@@ -47,6 +47,7 @@ import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.xml.util.ExceptionSimplifier;
 
 
 /**
@@ -138,9 +139,12 @@ public abstract class PRJDataStore extends URIDataStore {
                 listeners.warning(cannotReadAuxiliaryFile(extension));
                 return Optional.empty();
             }
-            if (content.source != null) {
+            if (content.source != null) try {
                 // ClassCastException handled by `catch` statement below.
                 return Optional.of(type.cast(readXML(content.source)));
+            } catch (JAXBException e) {
+                var s = new ExceptionSimplifier(content.getFilename(), e);
+                throw new DataStoreException(s.getMessage(getLocale()), s.exception);
             }
             final String wkt = content.toString();
             final StoreFormat format = new StoreFormat(dataLocale, timezone, null, listeners);
@@ -165,7 +169,7 @@ public abstract class PRJDataStore extends URIDataStore {
         } catch (NoSuchFileException | FileNotFoundException e) {
             listeners.warning(cannotReadAuxiliaryFile(extension), e);
             return Optional.empty();
-        } catch (IOException | ParseException | JAXBException | ClassCastException e) {
+        } catch (IOException | ParseException | ClassCastException e) {
             cause = e;
         }
         final var e = new DataStoreReferencingException(cannotReadAuxiliaryFile(extension), cause);
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
index 3427bb2b73..0862772199 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStore.java
@@ -51,6 +51,7 @@ import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.iso.Names;
 import org.apache.sis.xml.XML;
 import org.apache.sis.xml.util.URISource;
+import org.apache.sis.xml.util.ExceptionSimplifier;
 
 
 /**
@@ -328,31 +329,35 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R
      * @param  builder  where to merge the metadata.
      */
     protected final void mergeAuxiliaryMetadata(final MetadataBuilder builder) {
+        Object spec = null;         // Used only for formatting error message.
         Object metadata = null;
-        Exception error = null;
         try {
             final URI source;
             final InputStream input;
             final Path path = getMetadataPath();
             if (path != null) {
+                spec   = path;
                 source = path.toUri();
                 input  = open(path);
             } else {
                 source = getMetadataURI();
                 if (source == null) return;
+                spec  = source;
                 input = source.toURL().openStream();
             }
             metadata = readXML(input, source);
         } catch (URISyntaxException | IOException e) {
-            error = e;
+            listeners.warning(cannotReadAuxiliaryFile("xml"), e);
         } catch (JAXBException e) {
-            final Throwable cause = e.getCause();
-            error = (cause instanceof IOException) ? (Exception) cause : e;
+            Throwable cause = e.getCause();
+            if (cause instanceof IOException) {
+                listeners.warning(cannotReadAuxiliaryFile("xml"), (Exception) cause);
+            } else {
+                listeners.warning(new ExceptionSimplifier(spec, e).record(URIDataStore.class, "mergeAuxiliaryMetadata"));
+            }
         }
         if (metadata != null) {
             builder.mergeMetadata(metadata, getLocale());
-        } else if (error != null) {
-            listeners.warning(cannotReadAuxiliaryFile("xml"), error);
         }
     }
 
@@ -384,7 +389,7 @@ public abstract class URIDataStore extends DataStore implements StoreResource, R
         java.util.logging.Filter handler = (record) -> {
             record.setLoggerName(null);        // For allowing `listeners` to use the provider's logger name.
             listeners.warning(record);
-            return true;
+            return false;
         };
         // Cannot use Map.of(…) because it does not accept null values.
         Map<String,Object> properties = new HashMap<>(8);
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Exceptions.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Exceptions.java
index bfd796116e..f1d6514640 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Exceptions.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Exceptions.java
@@ -192,15 +192,11 @@ public final class Exceptions extends Static {
      *   <li>It is an instance of {@link BackingStoreException} (typically wrapping a checked exception).</li>
      *   <li>It is an instance of {@link UncheckedIOException} (wrapping a {@link java.io.IOException}).</li>
      *   <li>It is an instance of {@link DirectoryIteratorException} (wrapping a {@link java.io.IOException}).</li>
-     *   <li>It is a parent type of the cause. For example, some JDBC drivers wrap {@link SQLException}
-     *       in other {@code SQLException} without additional information.</li>
+     *   <li>It is a parent type of its cause. For example, some JDBC drivers wrap {@link SQLException} in another
+     *       {@code SQLException} without additional information. When the wrapper is a parent class of the cause,
+     *       details about the reason are less accessible.</li>
      * </ul>
      *
-     * <div class="note"><b>Note:</b>
-     * {@link java.security.PrivilegedActionException} is also a wrapper exception, but is not included in above list
-     * because it is used in very specific contexts. Furthermore, classes related to security manager are deprecated
-     * since Java 17.</div>
-     *
      * This method uses only the exception class as criterion;
      * it does not verify if the exception messages are the same.
      *
@@ -233,6 +229,9 @@ public final class Exceptions extends Static {
 
     /**
      * Unwraps and copies suppressed exceptions from the given source to the given target.
+     *
+     * @param  source  the exception from which to copy suppressed exceptions.
+     * @param  target  the exception where to add suppressed exceptions.
      */
     private static void copySuppressed(final Exception source, final Throwable target) {
         for (final Throwable suppressed : source.getSuppressed()) {
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
index dfe30d9602..0aba100be6 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
@@ -153,11 +153,21 @@ public class Errors extends IndexedResourceBundle {
          */
         public static final short CanNotRead_1 = 12;
 
+        /**
+         * Cannot read “{0}” at line {1}, column {2}.
+         */
+        public static final short CanNotRead_3 = 34;
+
         /**
          * Cannot represent “{1}” in a strictly standard-compliant {0} format.
          */
         public static final short CanNotRepresentInFormat_2 = 13;
 
+        /**
+         * Cannot resolve “{0}” as an absolute path.
+         */
+        public static final short CanNotResolveAsAbsolutePath_1 = 205;
+
         /**
          * Cannot set a value for parameter “{0}”.
          */
@@ -288,11 +298,6 @@ public class Errors extends IndexedResourceBundle {
          */
         public static final short ErrorInFileAtLine_2 = 33;
 
-        /**
-         * Error in “{0}”: {1}
-         */
-        public static final short ErrorIn_2 = 34;
-
         /**
          * A size of {1} elements is excessive for the “{0}” list.
          */
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
index 46fab02978..f134a437e3 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
@@ -42,7 +42,9 @@ CanNotParse_1                     = Cannot parse \u201c{0}\u201d.
 CanNotProcessProperty_2           = Cannot process property \u201c{0}\u201d. The reason is: {1}
 CanNotProcessPropertyAtPath_3     = Cannot process property \u201c{1}\u201d located at path \u201c{0}\u201d. The reason is: {2}
 CanNotRead_1                      = Cannot read \u201c{0}\u201d.
+CanNotRead_3                      = Cannot read \u201c{0}\u201d at line {1}, column {2}.
 CanNotReadPropertyInFile_2        = Cannot read property \u201c{1}\u201d in file \u201c{0}\u201d.
+CanNotResolveAsAbsolutePath_1     = Cannot resolve \u201c{0}\u201d as an absolute path.
 CanNotRepresentInFormat_2         = Cannot represent \u201c{1}\u201d in a strictly standard-compliant {0} format.
 CanNotSetParameterValue_1         = Cannot set a value for parameter \u201c{0}\u201d.
 CanNotSetPropertyValue_1          = Cannot set a value for property \u201c{0}\u201d.
@@ -69,7 +71,6 @@ EmptyArgument_1                   = Argument \u2018{0}\u2019 shall not be empty.
 EmptyDictionary                   = The dictionary shall contain at least one entry.
 EmptyEnvelope2D                   = Envelope must be at least two-dimensional and non-empty.
 EmptyProperty_1                   = Property named \u201c{0}\u201d shall not be empty.
-ErrorIn_2                         = Error in \u201c{0}\u201d: {1}
 ErrorInFileAtLine_2               = An error occurred in file \u201c{0}\u201d at line {1}.
 ExcessiveListSize_2               = A size of {1} elements is excessive for the \u201c{0}\u201d list.
 ExcessiveNumberOfDimensions_1     = For this algorithm, {0} is an excessive number of dimensions.
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
index c5cb2922ab..59f896eec0 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
@@ -39,7 +39,9 @@ CanNotParse_1                     = Ne peut pas interpr\u00e9ter \u00ab\u202f{0}
 CanNotProcessProperty_2           = Ne peut pas traiter la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb pour la raison suivante\u00a0: {1}
 CanNotProcessPropertyAtPath_3     = Ne peut pas traiter la propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb d\u00e9sign\u00e9e par le chemin \u00ab\u202f{0}\u202f\u00bb pour la raison suivante\u00a0: {2}
 CanNotRead_1                      = Ne peut pas lire \u00ab\u202f{0}\u202f\u00bb.
+CanNotRead_3                      = Ne peut pas lire \u00ab\u202f{0}\u202f\u00bb \u00e0 la ligne {1}, colonne {2}.
 CanNotReadPropertyInFile_2        = Ne peut pas lire la propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb dans le fichier \u00ab\u202f{0}\u202f\u00bb.
+CanNotResolveAsAbsolutePath_1     = Ne peut pas r\u00e9soudre \u00ab\u202f{0}\u202f\u00bb comme un chemin absolu.
 CanNotRepresentInFormat_2         = Ne peut pas repr\u00e9senter \u00ab\u202f{1}\u202f\u00bb dans un format {0} strictement conforme.
 CanNotSetParameterValue_1         = Ne peut pas d\u00e9finir une valeur pour le param\u00e8tre \u00ab\u202f{0}\u202f\u00bb.
 CanNotSetPropertyValue_1          = Ne peut pas d\u00e9finir une valeur pour la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb.
@@ -66,7 +68,6 @@ EmptyArgument_1                   = L\u2019argument \u2018{0}\u2019 ne doit pas
 EmptyDictionary                   = Le dictionnaire doit contenir au moins une entr\u00e9e.
 EmptyEnvelope2D                   = L\u2019enveloppe doit avoir au moins deux dimensions et ne pas \u00eatre vide.
 EmptyProperty_1                   = La propri\u00e9t\u00e9 nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb ne doit pas \u00eatre vide.
-ErrorIn_2                         = Erreur dans \u00ab\u202f{0}\u202f\u00bb\u00a0: {1}
 ErrorInFileAtLine_2               = Une erreur est survenue dans le fichier \u00ab\u202f{0}\u202f\u00bb \u00e0 la ligne {1}.
 ExcessiveListSize_2               = Une taille de {1} \u00e9l\u00e9ments est excessive pour la liste \u00ab\u202f{0}\u202f\u00bb.
 ExcessiveNumberOfDimensions_1     = Pour cet algorithme, {0} est un trop grand nombre de dimensions.
diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/ResourceInternationalString.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/ResourceInternationalString.java
index ca01478095..333e437107 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/ResourceInternationalString.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/ResourceInternationalString.java
@@ -113,10 +113,8 @@ public abstract class ResourceInternationalString extends AbstractInternationalS
      * @return a log record with the message of this international string.
      */
     public final LogRecord toLogRecord(final Level level) {
-        final LogRecord record = new LogRecord(level, getKeyConstants().getKeyName(key));
         final IndexedResourceBundle resources = getBundle(null);
-        record.setResourceBundleName(resources.getClass().getName());
-        record.setResourceBundle(resources);
+        final LogRecord record = resources.getLogRecord(level, key);
         if (hasArguments) {
             record.setParameters(resources.toArray(arguments));
         }