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 099e84507a Add an `--operation` parameter which can be used instead of 
`--sourceCRS` and `--targetCRS` in the command-line interface. This work 
required a refactoring of the way that auxiliary files are read in data stores, 
for reading both WKT and GML. As a side effect of this work, the PRJ files of 
World-File rasters can be in GML in addition of WKT.
099e84507a is described below

commit 099e84507a5cd6753d542f0cd83893520e5b72e4
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Jan 24 17:36:00 2024 +0100

    Add an `--operation` parameter which can be used instead of `--sourceCRS` 
and `--targetCRS` in the command-line interface.
    This work required a refactoring of the way that auxiliary files are read 
in data stores, for reading both WKT and GML.
    As a side effect of this work, the PRJ files of World-File rasters can be 
in GML in addition of WKT.
---
 .../org/apache/sis/console/OperationParser.java    |  66 +++
 .../main/org/apache/sis/console/Option.java        |   6 +
 .../main/org/apache/sis/console/Options.properties |   1 +
 .../org/apache/sis/console/Options_fr.properties   |   1 +
 .../main/org/apache/sis/console/SIS.java           |  11 +-
 .../org/apache/sis/console/TransformCommand.java   |  99 +++-
 .../main/org/apache/sis/xml/XML.java               |   9 +-
 .../main/org/apache/sis/xml/util/URISource.java    |   5 +-
 .../factory/sql/InstallationScriptProvider.java    |   2 +
 .../sis/storage/landsat/LandsatStoreProvider.java  |   4 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |   3 +-
 .../sis/storage/geotiff/GeoTiffStoreProvider.java  |   6 +-
 .../sis/storage/netcdf/NetcdfStoreProvider.java    |   4 +-
 .../sis/storage/xml/stream/StaxDataStore.java      |   2 +-
 .../main/org/apache/sis/io/stream/IOUtilities.java |   4 +-
 .../org/apache/sis/storage/DataStoreProvider.java  |   4 +-
 .../apache/sis/storage/base/AuxiliaryContent.java  | 196 +++++++
 .../sis/storage/base/DocumentedStoreProvider.java  |   2 +-
 .../apache/sis/storage/base/MetadataBuilder.java   |   3 +
 .../org/apache/sis/storage/base/PRJDataStore.java  | 323 ++---------
 .../org/apache/sis/storage/base/URIDataStore.java  | 595 +++++++++++----------
 .../sis/storage/base/URIDataStoreProvider.java     | 236 ++++++++
 .../main/org/apache/sis/storage/csv/Store.java     |   2 +-
 .../org/apache/sis/storage/csv/StoreProvider.java  |   4 +-
 .../org/apache/sis/storage/esri/RasterStore.java   |   6 +-
 .../apache/sis/storage/esri/RawRasterStore.java    |   5 +-
 .../apache/sis/storage/folder/StoreProvider.java   |  12 +-
 .../apache/sis/storage/image/WorldFileStore.java   |  14 +-
 .../org/apache/sis/storage/internal/Resources.java |   2 +-
 .../sis/storage/internal/Resources.properties      |   2 +-
 .../main/org/apache/sis/storage/wkt/Store.java     |   2 +-
 .../org/apache/sis/storage/wkt/StoreProvider.java  |   8 +-
 .../apache/sis/storage/xml/AbstractProvider.java   |  20 +
 .../coveragejson/CoverageJsonStoreProvider.java    |   4 +-
 .../org/apache/sis/gui/dataset/PathAction.java     |   4 +-
 .../org/apache/sis/gui/dataset/ResourceCell.java   |   4 +-
 36 files changed, 1051 insertions(+), 620 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/OperationParser.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/OperationParser.java
new file mode 100644
index 0000000000..d31192845c
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/OperationParser.java
@@ -0,0 +1,66 @@
+/*
+ * 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.console;
+
+import java.util.Optional;
+import org.opengis.metadata.Metadata;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.base.PRJDataStore;
+import org.opengis.referencing.operation.CoordinateOperation;
+
+
+/**
+ * Reads a coordinate operation in GML or WKT format.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class OperationParser extends PRJDataStore {
+    /**
+     * Creates a new parser for the given path or URL.
+     *
+     * @param  storage  path or URL (should not be a character string).
+     */
+    OperationParser(final Object storage) throws DataStoreException {
+        super(null, new StorageConnector(storage));
+    }
+
+    /**
+     * Access to the protected method from {@code PRJDataStore}.
+     *
+     * @return the coordinate operation, or empty if the file does not exist.
+     * @throws DataStoreException if an error occurred while reading the file.
+     */
+    final Optional<CoordinateOperation> read() throws DataStoreException {
+        return readWKT(CoordinateOperation.class, null);
+    }
+
+    /**
+     * Not used.
+     */
+    @Override
+    public Metadata getMetadata() {
+        return null;
+    }
+
+    /**
+     * Nothing to close.
+     */
+    @Override
+    public void close() {
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Option.java 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Option.java
index 8fdead8c96..3d42dd5a7c 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Option.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Option.java
@@ -37,6 +37,12 @@ enum Option {
      */
     TARGET_CRS(true),
 
+    /**
+     * The Coordinate Operation to apply on data.
+     * This option can be used as an alternative to the {@link #SOURCE_CRS} 
and {@link #TARGET_CRS} pair.
+     */
+    OPERATION(true),
+
     /**
      * Relative path to an auxiliary metadata file.
      */
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Options.properties
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Options.properties
index ae275090d8..b0a7d90e26 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Options.properties
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Options.properties
@@ -2,6 +2,7 @@
 # and to You under the Apache License, Version 2.0.
 sourceCRS=The Coordinate Reference System of input data.
 targetCRS=The Coordinate Reference System of output data.
+operation=The Coordinate Operation to apply on data (alternative to source and 
target CRS).
 metadata=Relative path to an auxiliary metadata file.
 output=The output file.
 format=The output format. Examples: xml, wkt, wkt1 or text.
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Options_fr.properties
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Options_fr.properties
index 6e7bf7227f..efffd7a511 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Options_fr.properties
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/Options_fr.properties
@@ -2,6 +2,7 @@
 # and to You under the Apache License, Version 2.0.
 sourceCRS=Le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es source.
 targetCRS=Le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es 
destination.
+operation=L\u2019op\u00e9ration \u00e0 appliquer sur les coordonn\u00e9es 
(alternative aux CRS source et destination).
 metadata=Chemin relatif vers un fichier auxiliaire de m\u00e9ta-donn\u00e9es.
 output=Le fichier de sortie.
 format=Le format de sortie. Exemples: xml, wkt, wkt1 ou text.
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/SIS.java 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/SIS.java
index 03e2c82c41..3b6a099470 100644
--- a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/SIS.java
+++ b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/SIS.java
@@ -596,11 +596,18 @@ public final class SIS extends Static {
      * <ul>
      *   <li>{@code --sourceCRS}: the coordinate reference system of input 
points.</li>
      *   <li>{@code --targetCRS}: the coordinate reference system of output 
points.</li>
+     *   <li>{@code --operation}: the coordinate operation from source CRS to 
target CRS.</li>
      * </ul>
      *
-     * Arguments other than options are files, usually as character strings 
but can also be
+     * The {@code --operation} parameter is optional.
+     * If provided, then the {@code --sourceCRS} and {@code --targetCRS} 
parameters become optional.
+     * If the operation is specified together with the source and/or target 
CRS, then the operation
+     * is used in the middle and conversions from/to the specified CRS are 
concatenated before/after
+     * the specified operation.
+     *
+     * <p>Arguments other than options are files, usually as character 
strings, but can also be
      * {@link java.io.File}, {@link java.nio.file.Path} or {@link 
java.net.URL} for example.
-     * Usage example:
+     * Usage example:</p>
      *
      * {@snippet lang="java" :
      *     
SIS.TRANSFORM.sourceCRS("EPSG:3395").targetCRS("EPSG:4326").run("data.txt");
diff --git 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TransformCommand.java
 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TransformCommand.java
index 8b4b6aff11..39975bbea8 100644
--- 
a/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TransformCommand.java
+++ 
b/endorsed/src/org.apache.sis.console/main/org/apache/sis/console/TransformCommand.java
@@ -45,6 +45,7 @@ import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.cs.CoordinateSystemAxis;
 import org.opengis.referencing.operation.SingleOperation;
 import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory;
 import org.opengis.referencing.operation.ConcatenatedOperation;
 import org.opengis.referencing.operation.PassThroughOperation;
 import org.opengis.referencing.operation.MathTransform;
@@ -57,7 +58,6 @@ import org.apache.sis.referencing.util.DirectPositionView;
 import org.apache.sis.referencing.util.ReferencingUtilities;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStores;
-import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.base.CodeType;
 import org.apache.sis.system.Modules;
@@ -74,6 +74,7 @@ import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.math.MathFunctions;
 import org.apache.sis.measure.Units;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.referencing.operation.DefaultCoordinateOperationFactory;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
@@ -91,8 +92,15 @@ import org.opengis.referencing.ObjectDomain;
  * <ul>
  *   <li>{@code --sourceCRS}: the coordinate reference system of input 
points.</li>
  *   <li>{@code --targetCRS}: the coordinate reference system of output 
points.</li>
+ *   <li>{@code --operation}: the coordinate operation from source CRS to 
target CRS.</li>
  * </ul>
  *
+ * The {@code --operation} parameter is optional.
+ * If provided, then the {@code --sourceCRS} and {@code --targetCRS} 
parameters become optional.
+ * If the operation is specified together with the source and/or target CRS, 
then the operation
+ * is used in the middle and conversions from/to the specified CRS are 
concatenated before/after
+ * the specified operation.
+ *
  * @author  Martin Desruisseaux (Geomatys)
  */
 final class TransformCommand extends FormattedOutputCommand {
@@ -150,7 +158,7 @@ final class TransformCommand extends FormattedOutputCommand 
{
      * Returns valid options for the {@code "transform"} commands.
      */
     private static EnumSet<Option> options() {
-        return EnumSet.of(Option.SOURCE_CRS, Option.TARGET_CRS, Option.VERBOSE,
+        return EnumSet.of(Option.SOURCE_CRS, Option.TARGET_CRS, 
Option.OPERATION, Option.VERBOSE,
                 Option.LOCALE, Option.TIMEZONE, Option.ENCODING, 
Option.COLORS, Option.HELP, Option.DEBUG);
     }
 
@@ -162,23 +170,53 @@ final class TransformCommand extends 
FormattedOutputCommand {
         resources = Vocabulary.getResources(locale);
     }
 
+    /**
+     * Creates the coordinate operation from the given authority code or file 
path.
+     * The result is assigned to the {@link #operation} field.
+     *
+     * @param  identifier  the authority code or file path.
+     * @throws InvalidOptionException if the coordinate operation cannot be 
read from the given identifier.
+     * @throws FactoryException if the operation failed for another reason.
+     */
+    private void fetchOperation(Object identifier) throws Exception {
+        if (identifier instanceof CharSequence) {
+            final String c = identifier.toString();
+            if (CodeType.guess(c).isCRS) try {
+                var factory = (CoordinateOperationAuthorityFactory) 
CRS.getAuthorityFactory(null);
+                operation = factory.createCoordinateOperation(c);
+                return;
+            } catch (NoSuchAuthorityCodeException e) {
+                throw illegalOptionValue(Option.OPERATION, identifier, e);
+            }
+            identifier = IOUtilities.toFileOrURL(c, "UTF-8");
+        }
+        final Object path = identifier;     // Because lambda function require 
effectively final variable.
+        if (path != null) {
+            operation = new OperationParser(path).read()
+                    .orElseThrow(() -> illegalOptionValue(Option.OPERATION, 
path, null));
+        }
+    }
+
     /**
      * Fetches the source or target coordinate reference system from the value 
given to the specified option.
      *
-     * @param  option  either {@link Option#SOURCE_CRS} or {@link 
Option#TARGET_CRS}.
+     * @param  option     either {@link Option#SOURCE_CRS} or {@link 
Option#TARGET_CRS}.
+     * @param  mandatory  whether the option is mandatory.
      * @return the coordinate reference system for the given option.
      * @throws InvalidOptionException if the given option is missing or have 
an invalid value.
      * @throws FactoryException if the operation failed for another reason.
      */
-    private CoordinateReferenceSystem fetchCRS(final Option option) throws 
InvalidOptionException, FactoryException, DataStoreException {
-        final Object identifier = getMandatoryOption(option);
+    private CoordinateReferenceSystem fetchCRS(final Option option, final 
boolean mandatory) throws Exception {
+        final Object identifier = mandatory ? getMandatoryOption(option) : 
options.get(option);
+        if (identifier == null) {
+            return null;
+        }
         if (identifier instanceof CharSequence) {
             final String c = identifier.toString();
             if (CodeType.guess(c).isCRS) try {
                 return CRS.forCode(c);
             } catch (NoSuchAuthorityCodeException e) {
-                final String name = option.label();
-                throw new 
InvalidOptionException(Errors.format(Errors.Keys.IllegalOptionValue_2, name, 
identifier), e, name);
+                throw illegalOptionValue(option, identifier, e);
             }
         }
         final Metadata metadata;
@@ -195,6 +233,14 @@ final class TransformCommand extends 
FormattedOutputCommand {
         throw new 
InvalidOptionException(Errors.format(Errors.Keys.UnspecifiedCRS), 
option.label());
     }
 
+    /**
+     * Creates the exception to throw for an illegal option value.
+     */
+    private static InvalidOptionException illegalOptionValue(Option option, 
Object identifier, Exception cause) {
+        final String name = option.label();
+        return new 
InvalidOptionException(Errors.format(Errors.Keys.IllegalOptionValue_2, name, 
identifier), cause, name);
+    }
+
     /**
      * Transforms coordinates from the files given in argument or from the 
standard input stream.
      *
@@ -202,8 +248,11 @@ final class TransformCommand extends 
FormattedOutputCommand {
      */
     @Override
     public int run() throws Exception {
-        final CoordinateReferenceSystem sourceCRS = 
fetchCRS(Option.SOURCE_CRS);
-        final CoordinateReferenceSystem targetCRS = 
fetchCRS(Option.TARGET_CRS);
+        fetchOperation(options.get(Option.OPERATION));
+        CoordinateReferenceSystem sourceCRS = fetchCRS(Option.SOURCE_CRS, 
operation == null);
+        CoordinateReferenceSystem targetCRS = fetchCRS(Option.TARGET_CRS, 
operation == null);
+        if (sourceCRS == null) sourceCRS = operation.getSourceCRS();    // May 
still be null.
+        if (targetCRS == null) targetCRS = operation.getTargetCRS();
         /*
          * Read all coordinates, so we can compute the area of interest.
          * This will be used when searching for a coordinate operation.
@@ -234,7 +283,35 @@ final class TransformCommand extends 
FormattedOutputCommand {
                 warning(e);
             }
         }
-        operation = CRS.findOperation(sourceCRS, targetCRS, areaOfInterest);
+        /*
+         * Now find or complete the coordinate operation. Note that the source 
and target CRS may be null
+         * if and only if a coordinate operation was explicitly specified. In 
that case, CRS are optional.
+         * If both a coordinate operation and CRS are present, this code 
ensures that the operation inputs
+         * and outputs match the specified CRS.
+         */
+        if (operation == null) {
+            operation = CRS.findOperation(sourceCRS, targetCRS, 
areaOfInterest);
+        } else {
+            final var steps = new ArrayList<CoordinateOperation>(3);
+            if (sourceCRS != null) {
+                CoordinateReferenceSystem step = operation.getSourceCRS();
+                if (step != null && step != sourceCRS) {
+                    steps.add(CRS.findOperation(sourceCRS, step, 
areaOfInterest));
+                }
+            }
+            steps.add(operation);
+            if (targetCRS != null) {
+                CoordinateReferenceSystem step = operation.getTargetCRS();
+                if (step != null && step != targetCRS) {
+                    steps.add(CRS.findOperation(step, targetCRS, 
areaOfInterest));
+                }
+            }
+            if (steps.size() > 1) {
+                var factory = DefaultCoordinateOperationFactory.provider();
+                var properties = IdentifiedObjects.getProperties(operation, 
CoordinateOperation.IDENTIFIERS_KEY);
+                operation = factory.createConcatenatedOperation(properties, 
steps.toArray(CoordinateOperation[]::new));
+            }
+        }
         /*
          * Prints the header: source CRS, target CRS, operation steps and 
positional accuracy.
          */
@@ -256,7 +333,7 @@ final class TransformCommand extends FormattedOutputCommand 
{
          * compute the number of digits to format and perform the actual 
coordinate operations.
          */
         if (!points.isEmpty()) {
-            coordinateWidth  = 15;                                      // 
Must be set before computeNumFractionDigits(…).
+            coordinateWidth  = 15;          // Must be set before 
computeNumFractionDigits(…).
             coordinateFormat = NumberFormat.getInstance(Locale.US);
             coordinateFormat.setGroupingUsed(false);
             
computeNumFractionDigits(operation.getTargetCRS().getCoordinateSystem());
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java
index f92dad0a07..eb707e2da7 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java
@@ -28,6 +28,7 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.io.BufferedInputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
@@ -47,6 +48,7 @@ import org.apache.sis.util.Workaround;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.system.Modules;
 import org.apache.sis.system.SystemListener;
+import org.apache.sis.xml.util.URISource;
 import org.apache.sis.xml.bind.TypeRegistration;
 import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
 
@@ -615,11 +617,8 @@ public final class XML extends Static {
     public static Object unmarshal(final Path input) throws JAXBException {
         ensureNonNull("input", input);
         final Object object;
-        try (InputStream in = Files.newInputStream(input, 
StandardOpenOption.READ)) {
-            final MarshallerPool pool = getPool();
-            final Unmarshaller unmarshaller = pool.acquireUnmarshaller();
-            object = unmarshaller.unmarshal(in);
-            pool.recycle(unmarshaller);
+        try (InputStream in = new 
BufferedInputStream(Files.newInputStream(input, StandardOpenOption.READ))) {
+            object = unmarshal(URISource.create(in, input.toUri()), null);
         } catch (IOException e) {
             throw new JAXBException(Errors.format(Errors.Keys.CanNotRead_1, 
input), e);
         }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/URISource.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/URISource.java
index cf8747830c..ec049dcfaf 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/URISource.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/URISource.java
@@ -67,6 +67,7 @@ public final class URISource extends StreamSource {
      */
     private URISource(final InputStream input, final URI source) {
         super(input);
+        // SystemId will be computed only if requested.
         document = source.normalize();
         fragment = null;
     }
@@ -78,7 +79,7 @@ public final class URISource extends StreamSource {
      * @param  source  URL of the XML document, or {@code null} if none.
      * @return the given input stream as a source.
      */
-    static StreamSource create(final InputStream input, final URI source) {
+    public static StreamSource create(final InputStream input, final URI 
source) {
         if (source != null) {
             return new URISource(input, source);
         } else {
@@ -87,7 +88,7 @@ public final class URISource extends StreamSource {
     }
 
     /**
-     * If this source if defined only by URI (no input stream), returns that 
URI.
+     * If this source is defined only by URI (no input stream), returns that 
URI.
      * Otherwise returns {@code null}.
      *
      * @return the URI, or {@code null} if not applicable for reading the 
document.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/InstallationScriptProvider.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/InstallationScriptProvider.java
index df9e33c62e..8c48379a51 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/InstallationScriptProvider.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/sql/InstallationScriptProvider.java
@@ -228,6 +228,7 @@ public abstract class InstallationScriptProvider extends 
InstallationResources {
      * Opens the input stream for the SQL script of the given name.
      * This method is invoked by the default implementation of {@link 
#openScript(String, int)}
      * for all scripts except {@link #PREPARE} and {@link #FINISH}.
+     * The returned input stream does not need to be buffered.
      *
      * <h4>Example 1</h4>
      * if this {@code InstallationScriptProvider} instance gets the SQL 
scripts from files in a well-known directory
@@ -365,6 +366,7 @@ public abstract class InstallationScriptProvider extends 
InstallationResources {
 
         /**
          * Opens the input stream for the SQL script of the given name.
+         * The returned input stream does not need to be buffered.
          *
          * @param  name  name of the script file to open.
          * @return an input stream opened of the given script file, or {@code 
null} if the resource was not found.
diff --git 
a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/LandsatStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/LandsatStoreProvider.java
index 58f2bd40e4..9ca9da35b9 100644
--- 
a/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/LandsatStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage.earthobservation/main/org/apache/sis/storage/landsat/LandsatStoreProvider.java
@@ -29,7 +29,7 @@ import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.ProbeResult;
 import org.apache.sis.storage.base.Capability;
 import org.apache.sis.storage.base.StoreMetadata;
-import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.storage.wkt.FirstKeywordPeek;
 
 
@@ -64,7 +64,7 @@ public class LandsatStoreProvider extends DataStoreProvider {
     /**
      * The parameter descriptor to be returned by {@link #getOpenParameters()}.
      */
-    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR = 
URIDataStore.Provider.descriptor(NAME);
+    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR = 
URIDataStoreProvider.descriptor(NAME);
 
     /**
      * The object to use for verifying if the first keyword is the expected 
one.
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index fc44ad6432..d5519f1420 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -50,6 +50,7 @@ import org.apache.sis.storage.IllegalNameException;
 import org.apache.sis.storage.base.MetadataBuilder;
 import org.apache.sis.storage.base.StoreUtilities;
 import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.storage.base.GridResourceWrapper;
 import org.apache.sis.storage.event.StoreEvent;
 import org.apache.sis.storage.event.StoreListener;
@@ -254,7 +255,7 @@ public class GeoTiffStore extends DataStore implements 
Aggregate {
         location    = connector.getStorageAs(URI.class);
         path        = connector.getStorageAs(Path.class);
         try {
-            if (URIDataStore.Provider.isWritable(connector, true)) {
+            if (URIDataStoreProvider.isWritable(connector, true)) {
                 ChannelDataOutput output = 
connector.commit(ChannelDataOutput.class, Constants.GEOTIFF);
                 writer = new Writer(this, output, 
connector.getOption(FormatModifier.OPTION_KEY));
             } else {
diff --git 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
index 573b40de9f..29ae8a2f19 100644
--- 
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStoreProvider.java
@@ -30,7 +30,7 @@ import org.apache.sis.storage.ProbeResult;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.base.StoreMetadata;
 import org.apache.sis.storage.base.Capability;
-import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.util.internal.Constants;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.parameter.ParameterBuilder;
@@ -91,7 +91,7 @@ public class GeoTiffStoreProvider extends DataStoreProvider {
         final var builder     = new ParameterBuilder();
         final var modifiers   = 
builder.addName(MODIFIERS).setDescription(Vocabulary.formatInternational(Vocabulary.Keys.Options)).create(FormatModifier[].class,
 null);
         final var compression = 
builder.addName(COMPRESSION).setDescription(Vocabulary.formatInternational(Vocabulary.Keys.Compression)).create(Compression.class,
 null);
-        OPEN_DESCRIPTOR = 
builder.addName(Constants.GEOTIFF).createGroup(URIDataStore.Provider.LOCATION_PARAM,
 modifiers, compression);
+        OPEN_DESCRIPTOR = 
builder.addName(Constants.GEOTIFF).createGroup(URIDataStoreProvider.LOCATION_PARAM,
 modifiers, compression);
     }
 
     /**
@@ -162,7 +162,7 @@ public class GeoTiffStoreProvider extends DataStoreProvider 
{
      */
     @Override
     public DataStore open(final StorageConnector connector) throws 
DataStoreException {
-        if (URIDataStore.Provider.isWritable(connector, false)) {
+        if (URIDataStoreProvider.isWritable(connector, false)) {
             return new WritableStore(this, connector);
         }
         return new GeoTiffStore(this, connector);
diff --git 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
index 1a40c27919..a3bdcff03c 100644
--- 
a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/NetcdfStoreProvider.java
@@ -44,7 +44,7 @@ import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.storage.base.StoreMetadata;
 import org.apache.sis.storage.base.Capability;
-import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.system.SystemListener;
 import org.apache.sis.system.Modules;
 import org.apache.sis.setup.GeometryLibrary;
@@ -98,7 +98,7 @@ public class NetcdfStoreProvider extends DataStoreProvider {
     /**
      * The parameter descriptor to be returned by {@link #getOpenParameters()}.
      */
-    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR = 
URIDataStore.Provider.descriptor(NAME);
+    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR = 
URIDataStoreProvider.descriptor(NAME);
 
     /**
      * The name of the {@link ucar.nc2.NetcdfFile} class, which is {@value}.
diff --git 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxDataStore.java
 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxDataStore.java
index d3d8fa93e1..538792945a 100644
--- 
a/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxDataStore.java
+++ 
b/endorsed/src/org.apache.sis.storage.xml/main/org/apache/sis/storage/xml/stream/StaxDataStore.java
@@ -338,7 +338,7 @@ public abstract class StaxDataStore extends URIDataStore {
          */
         @Override
         public boolean isLoggable(final LogRecord warning) {
-            warning.setLoggerName(null);        // For allowing `listeners` to 
select a logger name.
+            warning.setLoggerName(null);        // For allowing `listeners` to 
use the provider's logger name.
             listeners.warning(warning);
             return false;
         }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java
index c6eefebd10..f3d786aca5 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/io/stream/IOUtilities.java
@@ -206,8 +206,8 @@ public final class IOUtilities extends Static {
      */
     public static String toString(final Object path) {
         /*
-         * For the following types, the string that we want can be obtained 
only by toString(),
-         * or the class is final so we know that the toString(à behavior 
cannot be changed.
+         * For the following types, the string that we want can be obtained 
only by `toString()`,
+         * or the class is final so we know that the `toString()` behavior 
cannot be changed.
          */
         if (path instanceof CharSequence || path instanceof Path || path 
instanceof URL || path instanceof URI) {
             return path.toString();
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreProvider.java
index 0603c87f65..c6e338d1b6 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/DataStoreProvider.java
@@ -28,7 +28,7 @@ import org.apache.sis.metadata.simple.SimpleFormat;
 import org.apache.sis.metadata.iso.citation.DefaultCitation;
 import org.apache.sis.metadata.iso.distribution.DefaultFormat;
 import org.apache.sis.io.stream.Markable;
-import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.storage.internal.RewindableLineReader;
 import org.apache.sis.measure.Range;
 import org.apache.sis.util.Version;
@@ -611,7 +611,7 @@ public abstract class DataStoreProvider {
      */
     public DataStore open(final ParameterValueGroup parameters) throws 
DataStoreException {
         ArgumentChecks.ensureNonNull("parameter", parameters);
-        return open(URIDataStore.Provider.connector(this, parameters));
+        return open(URIDataStoreProvider.connector(this, parameters));
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/AuxiliaryContent.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/AuxiliaryContent.java
new file mode 100644
index 0000000000..e64c09006f
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/AuxiliaryContent.java
@@ -0,0 +1,196 @@
+/*
+ * 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.storage.base;
+
+import java.util.Arrays;
+import java.net.URL;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Path;
+import java.nio.charset.Charset;
+import javax.xml.transform.Source;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.io.stream.IOUtilities;
+
+
+/**
+ * Content of a data store auxiliary file.
+ * Instances of this class should be short lived, because they hold larger 
arrays than necessary.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see PRJDataStore#readAuxiliaryFile(String, boolean)
+ */
+public final class AuxiliaryContent implements CharSequence {
+    /**
+     * Buffer size and initial array capacity.
+     * We take a small buffer size because the files to read are typically 
small.
+     */
+    static final int BUFFER_SIZE = 1024;
+
+    /**
+     * Maximal length (in bytes) of auxiliary files. This is an arbitrary 
restriction, we could let
+     * the buffer growth indefinitely instead. But a large auxiliary file is 
probably an error and
+     * we do not want an {@link OutOfMemoryError} because of that.
+     */
+    private static final int MAXIMAL_LENGTH = 64 * 1024;
+
+    /**
+     * The XML source, or null if the file has not been identified as an XML 
file.
+     * This can be non-null only if the auxiliary file has been read with an
+     * {@code acceptXML} parameter set to {@code true}.
+     */
+    public final Source source;
+
+    /**
+     * {@link Path} or {@link URL} that have been read.
+     */
+    private final Object location;
+
+    /**
+     * The textual content of the auxiliary file.
+     */
+    private final char[] buffer;
+
+    /**
+     * Index of the first valid character in {@link #buffer}.
+     */
+    private final int offset;
+
+    /**
+     * Number of valid characters in {@link #buffer}.
+     */
+    private final int length;
+
+    /**
+     * Creates an auxiliary content for an XML file.
+     *
+     * @param location  {@link Path} or {@link URL} to read.
+     * @param source    the source to use for parsing the XML file.
+     */
+    AuxiliaryContent(final Object location, final Source source) {
+        this.source   = source;
+        this.location = location;
+        this.buffer   = ArraysExt.EMPTY_CHAR;
+        this.offset   = 0;
+        this.length   = 0;
+    }
+
+    /**
+     * Wraps (without copying) the given array as the content of an auxiliary 
file.
+     *
+     * @param location  {@link Path} or {@link URL} that have been read.
+     * @param buffer    the textual content of the auxiliary file.
+     * @param offset    index of the first valid character in {@code buffer}.
+     * @param length    number of valid characters in {@code buffer}.
+     */
+    private AuxiliaryContent(final Object location, final char[] buffer, final 
int offset, final int length) {
+        this.source   = null;
+        this.location = location;
+        this.buffer   = buffer;
+        this.offset   = offset;
+        this.length   = length;
+    }
+
+    /**
+     * Reads the content of the given input stream.
+     * This method closes the given stream.
+     *
+     * @param  location  {@link Path} or {@link URL} to read.
+     * @param  stream    input stream to read.
+     * @param  encoding  character encoding, or {@code null} for the default.
+     * @return the file content, or {@code null} if too large.
+     * @throws IOException if an error occurred while reading the stream.
+     */
+    static AuxiliaryContent read(final Object location, final InputStream 
stream, final Charset encoding) throws IOException {
+        try (InputStreamReader reader = (encoding != null)
+                ? new InputStreamReader(stream, encoding)
+                : new InputStreamReader(stream))
+        {
+            char[] buffer = new char[BUFFER_SIZE];
+            int offset = 0, count;
+            while ((count = reader.read(buffer, offset, buffer.length - 
offset)) >= 0) {
+                offset += count;
+                if (offset >= buffer.length) {
+                    if (offset >= MAXIMAL_LENGTH) return null;
+                    buffer = Arrays.copyOf(buffer, offset*2);
+                }
+            }
+            return new AuxiliaryContent(location, buffer, 0, offset);
+        }
+    }
+
+    /**
+     * Returns the filename (without path) of the auxiliary file.
+     * This information is mainly for producing error messages.
+     *
+     * @return name of the auxiliary file that have been read.
+     */
+    public String getFilename() {
+        return IOUtilities.filename(location);
+    }
+
+    /**
+     * Returns the source as an URI if possible.
+     *
+     * @return the source as an URI, or {@code null} if none.
+     * @throws URISyntaxException if the URI cannot be parsed.
+     */
+    public URI getURI() throws URISyntaxException {
+        return IOUtilities.toURI(location);
+    }
+
+    /**
+     * Returns the number of valid characters in this sequence.
+     */
+    @Override
+    public int length() {
+        return length;
+    }
+
+    /**
+     * Returns the character at the given index. For performance reasons this 
method does not check index bounds.
+     * The behavior of this method is undefined if the given index is not 
smaller than {@link #length()}.
+     * We skip bounds check because this class should be used for Apache SIS 
internal purposes only.
+     */
+    @Override
+    public char charAt(final int index) {
+        return buffer[offset + index];
+    }
+
+    /**
+     * Returns a sub-sequence of this auxiliary file content. For performance 
reasons this method does not
+     * perform bound checks. The behavior of this method is undefined if 
arguments are out of bounds.
+     * We skip bounds check because this class should be used for Apache SIS 
internal purposes only.
+     */
+    @Override
+    public CharSequence subSequence(final int start, final int end) {
+        return new AuxiliaryContent(location, buffer, offset + start, end - 
start);
+    }
+
+    /**
+     * Copies this auxiliary file content in a {@link String}.
+     * This method does not cache the result; caller should invoke at most 
once.
+     */
+    @Override
+    public String toString() {
+        return new String(buffer, offset, length);
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/DocumentedStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/DocumentedStoreProvider.java
index 75ef89addc..4fb34d1c62 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/DocumentedStoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/DocumentedStoreProvider.java
@@ -32,7 +32,7 @@ import org.apache.sis.system.Modules;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-public abstract class DocumentedStoreProvider extends URIDataStore.Provider {
+public abstract class DocumentedStoreProvider extends URIDataStoreProvider {
     /**
      * The primary key to use for searching in the {@code MD_Format} table, or 
{@code null} if none.
      * This primary name is also the value returned by {@link #getShortName()} 
default implementation.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
index c2c3b43030..83df0f4256 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java
@@ -84,6 +84,7 @@ import org.apache.sis.metadata.iso.spatial.*;
 import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.metadata.sql.MetadataSource;
 import org.apache.sis.metadata.internal.Merger;
+import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.AbstractResource;
 import org.apache.sis.storage.AbstractFeatureSet;
 import org.apache.sis.storage.AbstractGridCoverageResource;
@@ -837,6 +838,8 @@ public class MetadataBuilder {
      * @param  resource   the resource for which to add metadata.
      * @param  listeners  the listeners to notify in case of warning, or 
{@code null} if none.
      * @throws DataStoreException if an error occurred while reading metadata 
from the data store.
+     *
+     * @see #addTitleOrIdentifier(Resource)
      */
     public final void addDefaultMetadata(final AbstractResource resource, 
final StoreListeners listeners) throws DataStoreException {
         // Note: title is mandatory in ISO metadata, contrarily to the 
identifier.
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 0d0d46c253..dbfeba3ab8 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
@@ -16,25 +16,18 @@
  */
 package org.apache.sis.storage.base;
 
-import java.net.URL;
-import java.net.URI;
 import java.net.URISyntaxException;
-import java.net.UnknownServiceException;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.BufferedWriter;
 import java.io.FileNotFoundException;
-import java.nio.charset.Charset;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.NoSuchFileException;
 import java.text.ParseException;
 import java.text.ParsePosition;
 import java.util.Arrays;
-import java.util.Locale;
 import java.util.Optional;
-import java.util.TimeZone;
+import jakarta.xml.bind.JAXBException;
 import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.parameter.ParameterDescriptorGroup;
 import org.opengis.parameter.ParameterValueGroup;
@@ -43,11 +36,9 @@ import org.apache.sis.setup.OptionKey;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.DataStoreReferencingException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.internal.Resources;
-import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.storage.wkt.StoreFormat;
 import org.apache.sis.io.wkt.Convention;
 import org.apache.sis.parameter.ParameterBuilder;
@@ -61,23 +52,17 @@ import org.apache.sis.util.resources.Vocabulary;
 /**
  * A data store for a file or URI accompanied by an auxiliary file of the same 
name with {@code .prj} extension.
  * If the auxiliary file is absent, {@link OptionKey#DEFAULT_CRS} is used as a 
fallback.
- * The WKT 1 variant used for parsing the {@code "*.prj"} file is the variant 
used by "World Files" and GDAL;
- * this is not the standard specified by OGC 01-009 (they differ in there 
interpretation of units of measurement).
+ * The default WKT 1 variant used for parsing the {@code "*.prj"} file is the 
variant used by "World Files" and GDAL.
+ * This is not the standard specified by OGC 01-009 (they differ in there 
interpretation of units of measurement).
+ * This implementation accepts also WKT 2 (in which case the WKT 1 convention 
is ignored) and GML.
  *
- * <p>It is still possible to create a data store with a {@link 
java.nio.channels.ReadableByteChannel},
- * {@link java.io.InputStream} or {@link java.io.Reader}, in which case the 
{@linkplain #location} will
- * be null and the CRS defined by the {@code OptionKey} will be used.</p>
+ * <p>The URI can be null if the only available storage is a {@link 
java.nio.channels.ReadableByteChannel},
+ * {@link java.io.InputStream} or {@link java.io.Reader}. In such case, the 
CRS should be specified by the
+ * {@link OptionKey#DEFAULT_CRS}.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
 public abstract class PRJDataStore extends URIDataStore {
-    /**
-     * Maximal length (in bytes) of auxiliary files. This is an arbitrary 
restriction, we could let
-     * the buffer growth indefinitely instead. But a large auxiliary file is 
probably an error and
-     * we do not want an {@link OutOfMemoryError} because of that.
-     */
-    private static final int MAXIMAL_LENGTH = 64 * 1024;
-
     /**
      * The filename extension of {@code "*.prj"} files.
      *
@@ -85,25 +70,6 @@ public abstract class PRJDataStore extends URIDataStore {
      */
     protected static final String PRJ = "prj";
 
-    /**
-     * Character encoding in {@code *.prj} or other auxiliary files,
-     * or {@code null} for the JVM default (usually UTF-8).
-     */
-    protected final Charset encoding;
-
-    /**
-     * The locale for texts in {@code *.prj} or other auxiliary files,
-     * or {@code null} for {@link Locale#ROOT} (usually English).
-     * This locale is <strong>not</strong> used for parsing numbers or dates.
-     */
-    private final Locale locale;
-
-    /**
-     * Timezone for dates in {@code *.prj} or other auxiliary files,
-     * or {@code null} for UTC.
-     */
-    private final TimeZone timezone;
-
     /**
      * The coordinate reference system. This is initialized on the value 
provided by {@link OptionKey#DEFAULT_CRS}
      * at construction time, and is modified later if a {@code "*.prj"} file 
is found.
@@ -128,10 +94,18 @@ public abstract class PRJDataStore extends URIDataStore {
      */
     protected PRJDataStore(final DataStoreProvider provider, final 
StorageConnector connector) throws DataStoreException {
         super(provider, connector);
-        crs      = connector.getOption(OptionKey.DEFAULT_CRS);
-        encoding = connector.getOption(OptionKey.ENCODING);
-        locale   = connector.getOption(OptionKey.LOCALE);       // For 
`InternationalString`, not for numbers.
-        timezone = connector.getOption(OptionKey.TIMEZONE);
+        crs = connector.getOption(OptionKey.DEFAULT_CRS);
+    }
+
+    /**
+     * {@return the convention to use for parsing the PRJ file if Well-Known 
Text 1 is used}.
+     * Unfortunately, many formats use the ambiguous conventions from the very 
first specification,
+     * and ignore the clarifications done by OGC 01-009. In such case, we have 
to tell the WKT parser
+     * that the ambiguous conventions are used. This method can be overridden 
if the subclass has a way
+     * to know which WKT 1 conventions are used.
+     */
+    protected Convention getConvention() {
+        return Convention.WKT1_COMMON_UNITS;
     }
 
     /**
@@ -144,190 +118,61 @@ public abstract class PRJDataStore extends URIDataStore {
      * @throws DataStoreException if an error occurred while reading the file.
      */
     protected final void readPRJ() throws DataStoreException {
+        readWKT(CoordinateReferenceSystem.class, PRJ).ifPresent((result) -> 
crs = result);
+    }
+
+    /**
+     * Reads an auxiliary file in WKT or GML format. Standard PRJ files use 
WKT only,
+     * but the GML format is also accepted by this method as an extension 
specific to Apache SIS.
+     *
+     * @param  type       base class or interface of the object to read.
+     * @param  extension  extension of the file to read (usually {@link 
#PRJ}), or {@code null} for the main file.
+     * @return the parsed object, or an empty value if the file does not exist.
+     * @throws DataStoreException if an error occurred while reading the file.
+     */
+    protected final <T> Optional<T> readWKT(final Class<T> type, final String 
extension) throws DataStoreException {
         Exception cause = null, suppressed = null;
         try {
-            final AuxiliaryContent content = readAuxiliaryFile(PRJ);
+            final AuxiliaryContent content = readAuxiliaryFile(extension, 
true);
             if (content == null) {
-                listeners.warning(cannotReadAuxiliaryFile(PRJ));
-                return;
+                listeners.warning(cannotReadAuxiliaryFile(extension));
+                return Optional.empty();
+            }
+            if (content.source != null) {
+                // ClassCastException handled by `catch` statement below.
+                return Optional.of(type.cast(readXML(content.source)));
             }
             final String wkt = content.toString();
-            final StoreFormat format = new StoreFormat(locale, timezone, null, 
listeners);
-            format.setConvention(Convention.WKT1_COMMON_UNITS);         // 
Ignored if the format is WKT 2.
+            final StoreFormat format = new StoreFormat(dataLocale, timezone, 
null, listeners);
+            format.setConvention(getConvention());          // Ignored if the 
format is WKT 2.
             try {
                 format.setSourceFile(content.getURI());
             } catch (URISyntaxException e) {
                 suppressed = e;
             }
             final ParsePosition pos = new ParsePosition(0);
-            crs = (CoordinateReferenceSystem) format.parse(wkt, pos);
-            if (crs != null) {
+            // ClassCastException handled by `catch` statement below.
+            final T result = type.cast(format.parse(wkt, pos));
+            if (result != null) {
                 /*
                  * Some characters may exist after the WKT definition. For 
example, we sometimes see the CRS
                  * defined twice: as a WKT on the first line, followed by 
key-value pairs on next lines.
-                 * Current Apache SIS implementation ignores the characters 
after WKT.
+                 * Current Apache SIS implementation ignores all characters 
after the WKT.
                  */
-                format.validate(crs);
-                return;
+                format.validate(result);
+                return Optional.of(result);
             }
         } catch (NoSuchFileException | FileNotFoundException e) {
-            listeners.warning(cannotReadAuxiliaryFile(PRJ), e);
-            return;
-        } catch (IOException | ParseException | ClassCastException e) {
+            listeners.warning(cannotReadAuxiliaryFile(extension), e);
+            return Optional.empty();
+        } catch (IOException | ParseException | JAXBException | 
ClassCastException e) {
             cause = e;
         }
-        final var e = new 
DataStoreReferencingException(cannotReadAuxiliaryFile(PRJ), cause);
+        final var e = new 
DataStoreReferencingException(cannotReadAuxiliaryFile(extension), cause);
         if (suppressed != null) e.addSuppressed(suppressed);
         throw e;
     }
 
-    /**
-     * Reads the content of the auxiliary file with the specified extension.
-     * This method uses the same URI as {@link #location},
-     * except for the extension which is replaced by the given value.
-     * This method is suitable for reasonably small files.
-     * An arbitrary size limit is applied for safety.
-     *
-     * @param  extension    the filename extension of the auxiliary file to 
open.
-     * @return the file content together with the source, or {@code null} if 
none. Should be short-lived.
-     * @throws NoSuchFileException if the auxiliary file has not been found 
(when opened from path).
-     * @throws FileNotFoundException if the auxiliary file has not been found 
(when opened from URL).
-     * @throws IOException if another error occurred while opening the stream.
-     * @throws DataStoreException if the auxiliary file content seems too 
large.
-     */
-    protected final AuxiliaryContent readAuxiliaryFile(final String extension) 
throws IOException, DataStoreException {
-        /*
-         * Try to open the stream using the storage type (Path or URL) closest 
to the type
-         * given at construction time. We do that because those two types 
cannot open the
-         * same streams. For example, Path does not open HTTP or FTP 
connections by default,
-         * and URL does not open S3 files in current implementation.
-         */
-        final InputStream stream;
-        Path path = locationAsPath;
-        final Object source;                    // In case an error message is 
produced.
-        if (path != null) {
-            final String base = getBaseFilename(path);
-            path   = path.resolveSibling(base.concat(extension));
-            stream = Files.newInputStream(path);
-            source = path;
-        } else try {
-            final URI uri = IOUtilities.toAuxiliaryURI(location, extension, 
true);
-            if (uri == null) {
-                return null;
-            }
-            final URL url = uri.toURL();
-            stream = url.openStream();
-            source = url;
-        } catch (URISyntaxException e) {
-            throw new DataStoreException(cannotReadAuxiliaryFile(extension), 
e);
-        }
-        /*
-         * Reads the auxiliary file fully, with an arbitrary size limit.
-         */
-        try (InputStreamReader reader = (encoding != null)
-                ? new InputStreamReader(stream, encoding)
-                : new InputStreamReader(stream))
-        {
-            char[] buffer = new char[1024];
-            int offset = 0, count;
-            while ((count = reader.read(buffer, offset, buffer.length - 
offset)) >= 0) {
-                offset += count;
-                if (offset >= buffer.length) {
-                    if (offset >= MAXIMAL_LENGTH) {
-                        throw new 
DataStoreContentException(Resources.forLocale(listeners.getLocale())
-                                
.getString(Resources.Keys.AuxiliaryFileTooLarge_1, 
IOUtilities.filename(source)));
-                    }
-                    buffer = Arrays.copyOf(buffer, offset*2);
-                }
-            }
-            return new AuxiliaryContent(source, buffer, 0, offset);
-        }
-    }
-
-    /**
-     * Content of a file read by {@link #readAuxiliaryFile(String)}.
-     * This is used as a workaround for not being able to return multiple 
values from a single method.
-     * Instances of this class should be short lived, because they hold larger 
arrays than necessary.
-     */
-    protected static final class AuxiliaryContent implements CharSequence {
-        /** {@link Path} or {@link URL} that have been read. */
-        private final Object source;
-
-        /** The textual content of the auxiliary file. */
-        private final char[] buffer;
-
-        /** Index of the first valid character in {@link #buffer}. */
-        private final int offset;
-
-        /** Number of valid characters in {@link #buffer}. */
-        private final int length;
-
-        /** Wraps (without copying) the given array as the content of an 
auxiliary file. */
-        private AuxiliaryContent(final Object source, final char[] buffer, 
final int offset, final int length) {
-            this.source = source;
-            this.buffer = buffer;
-            this.offset = offset;
-            this.length = length;
-        }
-
-        /**
-         * Returns the filename (without path) of the auxiliary file.
-         * This information is mainly for producing error messages.
-         *
-         * @return name of the auxiliary file that have been read.
-         */
-        public String getFilename() {
-            return IOUtilities.filename(source);
-        }
-
-        /**
-         * Returns the source as an URI if possible.
-         *
-         * @return the source as an URI, or {@code null} if none.
-         * @throws URISyntaxException if the URI cannot be parsed.
-         */
-        public URI getURI() throws URISyntaxException {
-            return IOUtilities.toURI(source);
-        }
-
-        /**
-         * Returns the number of valid characters in this sequence.
-         */
-        @Override
-        public int length() {
-            return length;
-        }
-
-        /**
-         * Returns the character at the given index. For performance reasons 
this method does not check index bounds.
-         * The behavior of this method is undefined if the given index is not 
smaller than {@link #length()}.
-         * We skip bounds check because this class should be used for Apache 
SIS internal purposes only.
-         */
-        @Override
-        public char charAt(final int index) {
-            return buffer[offset + index];
-        }
-
-        /**
-         * Returns a sub-sequence of this auxiliary file content. For 
performance reasons this method does not
-         * perform bound checks. The behavior of this method is undefined if 
arguments are out of bounds.
-         * We skip bounds check because this class should be used for Apache 
SIS internal purposes only.
-         */
-        @Override
-        public CharSequence subSequence(final int start, final int end) {
-            return new AuxiliaryContent(source, buffer, offset + start, end - 
start);
-        }
-
-        /**
-         * Copies this auxiliary file content in a {@link String}.
-         * This method does not cache the result; caller should invoke at most 
once.
-         */
-        @Override
-        public String toString() {
-            return new String(buffer, offset, length);
-        }
-    }
-
     /**
      * Writes the {@code "*.prj"} auxiliary file if {@link #crs} is non-null.
      * If {@link #crs} is null and the auxiliary file exists, it is deleted.
@@ -346,7 +191,7 @@ public abstract class PRJDataStore extends URIDataStore {
             if (crs == null) {
                 deleteAuxiliaryFile(PRJ);
             } else try (BufferedWriter out = writeAuxiliaryFile(PRJ)) {
-                final StoreFormat format = new StoreFormat(locale, timezone, 
null, listeners);
+                final StoreFormat format = new StoreFormat(dataLocale, 
timezone, null, listeners);
                 // Keep the default "WKT 2" format (see method javadoc).
                 format.format(crs, out);
                 out.newLine();
@@ -358,48 +203,6 @@ public abstract class PRJDataStore extends URIDataStore {
         }
     }
 
-    /**
-     * Creates a writer for an auxiliary file with the specified extension.
-     * This method uses the same path as {@link #location},
-     * except for the extension which is replaced by the given value.
-     *
-     * @param  extension  the filename extension of the auxiliary file to 
write.
-     * @return a stream opened on the specified file.
-     * @throws UnknownServiceException if no {@link Path} or {@link 
java.net.URI} is available.
-     * @throws DataStoreException if the auxiliary file cannot be created.
-     * @throws IOException if another error occurred while opening the stream.
-     */
-    protected final BufferedWriter writeAuxiliaryFile(final String extension)
-            throws IOException, DataStoreException
-    {
-        final Path[] paths = super.getComponentFiles();
-        if (paths.length == 0) {
-            throw new UnknownServiceException();
-        }
-        Path path = paths[0];
-        final String base = getBaseFilename(path);
-        path = path.resolveSibling(base.concat(extension));
-        return (encoding != null)
-                ? Files.newBufferedWriter(path, encoding)
-                : Files.newBufferedWriter(path);
-    }
-
-    /**
-     * Deletes the auxiliary file with the given extension if it exists.
-     * If the auxiliary file does not exist, then this method does nothing.
-     *
-     * @param  extension  the filename extension of the auxiliary file to 
delete.
-     * @throws DataStoreException if the auxiliary file is not on a supported 
file system.
-     * @throws IOException if an error occurred while deleting the file.
-     */
-    protected final void deleteAuxiliaryFile(final String extension) throws 
DataStoreException, IOException {
-        for (Path path : super.getComponentFiles()) {
-            final String base = getBaseFilename(path);
-            path = path.resolveSibling(base.concat(extension));
-            Files.deleteIfExists(path);
-        }
-    }
-
     /**
      * Returns the {@linkplain #location} as a {@code Path} component together 
with auxiliary files.
      * The default implementation does the same computation as the 
super-class, then adds the sibling
@@ -447,17 +250,6 @@ public abstract class PRJDataStore extends URIDataStore {
         return paths;
     }
 
-    /**
-     * Returns the filename of the given path without the file suffix.
-     * The returned string always ends in {@code '.'}, making it ready
-     * for concatenation of a new suffix.
-     */
-    private static String getBaseFilename(final Path path) {
-        final String base = path.getFileName().toString();
-        final int s = base.lastIndexOf(IOUtilities.EXTENSION_SEPARATOR);
-        return (s >= 0) ? base.substring(0, s+1) : base + 
IOUtilities.EXTENSION_SEPARATOR;
-    }
-
     /**
      * Returns the parameters used to open this data store.
      *
@@ -465,12 +257,9 @@ public abstract class PRJDataStore extends URIDataStore {
      */
     @Override
     public Optional<ParameterValueGroup> getOpenParameters() {
-        final ParameterValueGroup pg = parameters(provider, location);
-        if (pg != null) {
-            pg.parameter(Provider.CRS_NAME).setValue(crs);
-            return Optional.of(pg);
-        }
-        return Optional.empty();
+        final Optional<ParameterValueGroup> op = super.getOpenParameters();
+        op.ifPresent((pg) -> pg.parameter(Provider.CRS_NAME).setValue(crs));
+        return op;
     }
 
     /**
@@ -478,7 +267,7 @@ public abstract class PRJDataStore extends URIDataStore {
      *
      * @author  Martin Desruisseaux (Geomatys)
      */
-    public abstract static class Provider extends URIDataStore.Provider {
+    public abstract static class Provider extends URIDataStoreProvider {
         /**
          * Name of the {@link #DEFAULT_CRS} parameter.
          */
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 88a51b19a0..3427bb2b73 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
@@ -16,47 +16,49 @@
  */
 package org.apache.sis.storage.base;
 
+import java.util.Map;
+import java.util.HashMap;
 import java.util.Arrays;
 import java.util.Optional;
-import java.io.DataInput;
-import java.io.DataOutput;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.io.BufferedWriter;
+import java.io.BufferedInputStream;
 import java.io.InputStream;
-import java.io.OutputStream;
 import java.io.IOException;
+import java.net.URL;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.nio.file.Files;
-import java.nio.file.OpenOption;
 import java.nio.file.StandardOpenOption;
 import java.nio.charset.Charset;
+import javax.xml.transform.Source;
 import jakarta.xml.bind.JAXBException;
 import org.opengis.util.GenericName;
 import org.opengis.parameter.ParameterValueGroup;
-import org.opengis.parameter.ParameterDescriptor;
-import org.opengis.parameter.ParameterDescriptorGroup;
-import org.opengis.parameter.ParameterNotFoundException;
-import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataOptionKey;
-import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreProvider;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.IllegalOpenParameterException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.storage.ReadOnlyStorageException;
 import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.setup.OptionKey;
 import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.iso.Names;
-import org.apache.sis.util.logging.Logging;
 import org.apache.sis.xml.XML;
+import org.apache.sis.xml.util.URISource;
 
 
 /**
  * A data store for a storage that may be represented by a {@link URI}.
- * It is still possible to create a data store with a {@link 
java.nio.channels.ReadableByteChannel},
- * {@link java.io.InputStream} or {@link java.io.Reader}, in which case the 
{@linkplain #location} will be null.
+ * The URI is stored in {@link #location} field and is used for populating 
some default metadata.
+ * It is also use for resolving the path to auxiliary files, for example the 
CRS definition in PRJ file.
+ * The URI can be null if the only available storage is a {@link 
java.nio.channels.ReadableByteChannel},
+ * {@link InputStream} or {@link java.io.Reader}.
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
@@ -82,11 +84,30 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource, R
      */
     private final Path metadataPath;
 
+    /**
+     * User-specified character encoding, or {@code null} for the JVM default 
(usually UTF-8).
+     * Subclasses may replace this value by a value read from the data file.
+     */
+    protected Charset encoding;
+
+    /**
+     * User-specified locale for textual content, or {@code null} for {@link 
Locale#ROOT} (usually English).
+     * This locale is usually <strong>not</strong> used for parsing numbers or 
dates.
+     * Subclasses may replace this value by a value read from the data file.
+     */
+    protected Locale dataLocale;
+
+    /**
+     * User-specified timezone for dates, or {@code null} for UTC.
+     * Subclasses may replace this value by a value read from the data file.
+     */
+    protected TimeZone timezone;
+
     /**
      * Creates a new data store. This constructor does not open the file,
      * so subclass constructors can decide whether to open in read-only or 
read/write mode.
      * It is caller's responsibility to ensure that the {@link 
java.nio.file.OpenOption}
-     * are compatible with whether this data store is read-only or read/write.
+     * are compatible with the capabilities (read-only or read/write) of this 
data store.
      *
      * @param  provider   the factory that created this {@code URIDataStore} 
instance, or {@code null} if unspecified.
      * @param  connector  information about the storage (URL, stream, reader 
instance, <i>etc</i>).
@@ -101,6 +122,9 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource, R
         } else {
             metadataPath = null;
         }
+        encoding   = connector.getOption(OptionKey.ENCODING);
+        dataLocale = connector.getOption(OptionKey.LOCALE);
+        timezone   = connector.getOption(OptionKey.TIMEZONE);
     }
 
     /**
@@ -127,9 +151,19 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource, R
     }
 
     /**
-     * Returns the filename without path and without file extension, or {@code 
null} if none.
+     * {@return the filename without path and without file extension, or null 
if none}.
+     * This method can be used for building metadata like below (note that 
{@link #getIdentifier()}
+     * should not be invoked during metadata construction time, for avoiding 
recursive method calls):
+     *
+     * {@snippet lang="java" :
+     *     builder.addTitleOrIdentifier(getFilename(), 
MetadataBuilder.Scope.ALL);
+     *     }
+     *
+     * Above snippet should not be applied before this data store did its best 
effort for providing a title.
+     * The use of identifier as a title is a fallback for making valid 
metadata, because the title is mandatory
+     * in ISO 19111 metadata.
      */
-    private String getFilename() {
+    public final String getFilename() {
         if (location == null) {
             return null;
         }
@@ -138,46 +172,64 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource, R
     }
 
     /**
-     * {@return the path to the auxiliary metadata file, or {@code null} if 
none}.
-     * This is a path built from the {@link DataOptionKey#METADATA_PATH} value 
if present.
-     * Note that the metadata may be unavailable as a {@link Path} but 
available as an {@link URI}.
+     * Returns the main and metadata locations as {@code Path} components, or 
an empty array if none.
+     * The default implementation returns the storage specified at 
construction time converted to a
+     * {@link Path} if such conversion was possible, or an empty array 
otherwise. The array may also
+     * contains the path to the {@linkplain DataOptionKey#METADATA_PATH 
auxiliary metadata file}.
+     *
+     * @return the URI to component files as paths, or an empty array if 
unknown.
+     * @throws DataStoreException if an error occurred while getting the paths.
      */
-    private Path getMetadataPath() throws IOException {
-        Path path = replaceWildcard(metadataPath);
-        if (path != null) {
-            Path parent = locationAsPath;
-            if (parent != null) {
-                parent = parent.getParent();
-                if (parent != null) {
-                    path = parent.resolve(path);
-                }
-            }
-            if (Files.isSameFile(path, locationAsPath)) {
-                return null;
-            }
+    @Override
+    public Path[] getComponentFiles() throws DataStoreException {
+        try {
+            final var paths = new Path[] {locationAsPath, getMetadataPath()};
+            return ArraysExt.resize(paths, ArraysExt.removeDuplicated(paths, 
ArraysExt.removeNulls(paths)));
+        } catch (IOException e) {
+            throw new DataStoreException(e);
         }
-        return path;
     }
 
     /**
-     * {@return the URI to the auxiliary metadata file, or {@code null} if 
none}.
-     * This is a path built from the {@link DataOptionKey#METADATA_PATH} value 
if present.
-     * Note that the metadata may be unavailable as an {@link URI} but 
available as a {@link Path}.
+     * Returns the parameters used to open this data store.
+     *
+     * @return parameters used for opening this {@code DataStore}.
      */
-    private URI getMetadataURI() throws URISyntaxException {
-        URI uri = location;
-        if (uri != null) {
-            final Path path = replaceWildcard(metadataPath);
-            if (path != null) {
-                uri = IOUtilities.toAuxiliaryURI(uri, path.toString(), false);
-                if (!uri.equals(location)) {
-                    return uri;
-                }
-            }
-        }
-        return null;
+    @Override
+    public Optional<ParameterValueGroup> getOpenParameters() {
+        return Optional.ofNullable(parameters(provider, location));
     }
 
+    /**
+     * Creates parameter value group for the current location, if non-null.
+     * This convenience method is used for {@link 
DataStore#getOpenParameters()} implementations in public
+     * {@code DataStore} that cannot extend {@code URIDataStore} directly, 
because this class is internal.
+     *
+     * @param  provider  the provider of the data store for which to get open 
parameters.
+     * @param  location  file opened by the data store.
+     * @return parameters to be returned by {@link 
DataStore#getOpenParameters()}, or {@code null} if unknown.
+     */
+    public static ParameterValueGroup parameters(final DataStoreProvider 
provider, final URI location) {
+        if (location == null || provider == null) return null;
+        final ParameterValueGroup pg = 
provider.getOpenParameters().createValue();
+        pg.parameter(DataStoreProvider.LOCATION).setValue(location);
+        return pg;
+    }
+
+
+
+
+    /*
+     
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+     ┃                                                                         
       ┃
+     ┃                                AUXILIARY FILES                          
       ┃
+     ┃                                                                         
       ┃
+     ┃      The following are helper methods for data stores made of many 
files       ┃
+     ┃      at some location relative to the main file (e.g. in the same 
folder).     ┃
+     ┃                                                                         
       ┃
+     
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+     */
+
     /**
      * Returns the given path with the wildcard character replaced by the name 
of the main file.
      *
@@ -217,297 +269,262 @@ public abstract class URIDataStore extends DataStore 
implements StoreResource, R
     }
 
     /**
-     * Returns the main and metadata locations as {@code Path} components, or 
an empty array if none.
-     * The default implementation returns the storage specified at 
construction time converted to a {@link Path}
-     * if such conversion was possible, or {@code null} otherwise.
-     *
-     * @return the URI as a path, or an empty array if unknown.
-     * @throws DataStoreException if an error occurred while getting the paths.
+     * {@return the path to the auxiliary metadata file, or null if none}.
+     * This is a path built from the {@link DataOptionKey#METADATA_PATH} value 
if present.
+     * Note that the metadata may be unavailable as a {@link Path} but 
available as an {@link URI}.
      */
-    @Override
-    public Path[] getComponentFiles() throws DataStoreException {
-        try {
-            final var paths = new Path[] {locationAsPath, getMetadataPath()};
-            return ArraysExt.resize(paths, ArraysExt.removeDuplicated(paths, 
ArraysExt.removeNulls(paths)));
-        } catch (IOException e) {
-            throw new DataStoreException(e);
+    private Path getMetadataPath() throws IOException {
+        Path path = replaceWildcard(metadataPath);
+        if (path != null) {
+            Path parent = locationAsPath;
+            if (parent != null) {
+                parent = parent.getParent();
+                if (parent != null) {
+                    path = parent.resolve(path);
+                }
+            }
+            if (Files.isSameFile(path, locationAsPath)) {
+                return null;
+            }
         }
+        return path;
     }
 
     /**
-     * Returns the parameters used to open this data store.
-     *
-     * @return parameters used for opening this {@code DataStore}.
+     * {@return the URI to the auxiliary metadata file, or null if none}.
+     * This is a path built from the {@link DataOptionKey#METADATA_PATH} value 
if present.
+     * Note that the metadata may be unavailable as an {@link URI} but 
available as a {@link Path}.
      */
-    @Override
-    public Optional<ParameterValueGroup> getOpenParameters() {
-        return Optional.ofNullable(parameters(provider, location));
+    private URI getMetadataURI() throws URISyntaxException {
+        URI uri = location;
+        if (uri != null) {
+            final Path path = replaceWildcard(metadataPath);
+            if (path != null) {
+                uri = IOUtilities.toAuxiliaryURI(uri, path.toString(), false);
+                if (!uri.equals(location)) {
+                    return uri;
+                }
+            }
+        }
+        return null;
     }
 
     /**
-     * Creates parameter value group for the current location, if non-null.
-     * This convenience method is used for {@link 
DataStore#getOpenParameters()} implementations in public
-     * {@code DataStore} that cannot extend {@code URIDataStore} directly, 
because this class is internal.
-     *
-     * @param  provider  the provider of the data store for which to get open 
parameters.
-     * @param  location  file opened by the data store.
-     * @return parameters to be returned by {@link 
DataStore#getOpenParameters()}.
+     * Opens a buffered input stream for the given path.
      *
-     * @todo Verify if non-exported classes in JDK9 are hidden from Javadoc, 
like package-private classes.
-     *       If true, we could remove this hack and extend {@code 
URIDataStore} even in public classes.
+     * @param  path  the path to the file to open.
+     * @return a new buffered input stream.
+     * @throws IOException in an error occurred while opening the file.
      */
-    public static ParameterValueGroup parameters(final DataStoreProvider 
provider, final URI location) {
-        if (location == null || provider == null) return null;
-        final ParameterValueGroup pg = 
provider.getOpenParameters().createValue();
-        pg.parameter(DataStoreProvider.LOCATION).setValue(location);
-        return pg;
+    private static InputStream open(final Path path) throws IOException {
+        return new BufferedInputStream(Files.newInputStream(path, 
StandardOpenOption.READ), AuxiliaryContent.BUFFER_SIZE);
     }
 
     /**
-     * Provider for {@link URIDataStore} instances.
+     * If an auxiliary metadata file has been specified, merges that file to 
the given metadata.
+     * This step should be done only after the data store added its own 
metadata.
+     * Failure to load auxiliary metadata are only a warning.
      *
-     * @author  Johann Sorel (Geomatys)
-     * @author  Martin Desruisseaux (Geomatys)
+     * @param  builder  where to merge the metadata.
      */
-    public abstract static class Provider extends DataStoreProvider {
-        /**
-         * Description of the {@value #LOCATION} parameter.
-         */
-        public static final ParameterDescriptor<URI> LOCATION_PARAM;
-
-        /**
-         * Description of the "metadata" parameter.
-         */
-        public static final ParameterDescriptor<Path> METADATA_PARAM;
-
-        /**
-         * Description of the optional {@value #CREATE} parameter, which may 
be present in writable data store.
-         * This parameter is not included in the descriptor created by {@link 
#build(ParameterBuilder)} default
-         * implementation. It is subclass responsibility to add it if desired, 
only if supported.
-         */
-        public static final ParameterDescriptor<Boolean> CREATE_PARAM;
-
-        /**
-         * Description of the optional parameter for character encoding used 
by the data store.
-         * This parameter is not included in the descriptor created by {@link 
#build(ParameterBuilder)}
-         * default implementation. It is subclass responsibility to add it if 
desired.
-         */
-        public static final ParameterDescriptor<Charset> ENCODING;
-        static {
-            final ParameterBuilder builder = new ParameterBuilder();
-            ENCODING       = 
builder.addName("encoding").setDescription(Resources.formatInternational(Resources.Keys.DataStoreEncoding)).create(Charset.class,
 null);
-            CREATE_PARAM   = builder.addName( CREATE   
).setDescription(Resources.formatInternational(Resources.Keys.DataStoreCreate  
)).create(Boolean.class, null);
-            METADATA_PARAM = 
builder.addName("metadata").setDescription(Resources.formatInternational(Resources.Keys.MetadataLocation
 )).create(Path.class, null);
-            LOCATION_PARAM = builder.addName( LOCATION 
).setDescription(Resources.formatInternational(Resources.Keys.DataStoreLocation)).setRequired(true).create(URI.class,
 null);
-        }
-
-        /**
-         * The parameter descriptor to be returned by {@link 
#getOpenParameters()}.
-         * Created when first needed.
-         */
-        private volatile ParameterDescriptorGroup openDescriptor;
-
-        /**
-         * Creates a new provider.
-         */
-        protected Provider() {
-        }
-
-        /**
-         * Returns a description of all parameters accepted by this provider 
for opening a data store.
-         * This method creates the descriptor only when first needed. 
Subclasses can override the
-         * {@link #build(ParameterBuilder)} method if they need to modify the 
descriptor to create.
-         *
-         * @return description of the parameters required or accepted for 
opening a {@link DataStore}.
-         */
-        @Override
-        public final ParameterDescriptorGroup getOpenParameters() {
-            ParameterDescriptorGroup desc = openDescriptor;
-            if (desc == null) {
-                openDescriptor = desc = build(new 
ParameterBuilder().addName(getShortName()));
+    protected final void mergeAuxiliaryMetadata(final MetadataBuilder builder) 
{
+        Object metadata = null;
+        Exception error = null;
+        try {
+            final URI source;
+            final InputStream input;
+            final Path path = getMetadataPath();
+            if (path != null) {
+                source = path.toUri();
+                input  = open(path);
+            } else {
+                source = getMetadataURI();
+                if (source == null) return;
+                input = source.toURL().openStream();
             }
-            return desc;
-        }
-
-        /**
-         * Invoked by {@link #getOpenParameters()} the first time that a 
parameter descriptor needs to be created.
-         * When invoked, the parameter group name is set to a name derived 
from the {@link #getShortName()} value.
-         * The default implementation creates a group containing {@link 
#LOCATION_PARAM} and {@link #METADATA_PARAM}.
-         * Subclasses can override if they need to create a group with more 
parameters.
-         *
-         * @param  builder  the builder to use for creating parameter 
descriptor. The group name is already set.
-         * @return the parameters descriptor created from the given builder.
-         */
-        protected ParameterDescriptorGroup build(final ParameterBuilder 
builder) {
-            return builder.createGroup(LOCATION_PARAM, METADATA_PARAM);
+            metadata = readXML(input, source);
+        } catch (URISyntaxException | IOException e) {
+            error = e;
+        } catch (JAXBException e) {
+            final Throwable cause = e.getCause();
+            error = (cause instanceof IOException) ? (Exception) cause : e;
         }
-
-        /**
-         * Convenience method creating a parameter descriptor containing only 
{@link #LOCATION_PARAM}.
-         * This convenience method is used for public providers that cannot 
extend this {@code Provider}
-         * class because it is internal.
-         *
-         * @param  name  short name of the data store format.
-         * @return the descriptor for open parameters.
-         *
-         * @todo Verify if non-exported classes in JDK9 are hidden from 
Javadoc, like package-private classes.
-         *       If true, we could remove this hack and extend {@code 
URIDataStore} even in public classes.
-         */
-        public static ParameterDescriptorGroup descriptor(final String name) {
-            return new 
ParameterBuilder().addName(name).createGroup(LOCATION_PARAM);
+        if (metadata != null) {
+            builder.mergeMetadata(metadata, getLocale());
+        } else if (error != null) {
+            listeners.warning(cannotReadAuxiliaryFile("xml"), error);
         }
+    }
 
-        /**
-         * Creates a storage connector initialized to the location declared in 
given parameters.
-         * This convenience method does not set any other parameters.
-         * In particular, reading (or ignoring) the {@value #CREATE} parameter 
is left to callers,
-         * because not all implementations may create data stores with {@link 
java.nio.file.StandardOpenOption}.
-         *
-         * @param  provider    the provider for which to create a storage 
connector (for error messages).
-         * @param  parameters  the parameters to use for creating a storage 
connector.
-         * @return the storage connector initialized to the location specified 
in the parameters.
-         * @throws IllegalOpenParameterException if no {@value #LOCATION} 
parameter has been found.
-         */
-        public static StorageConnector connector(final DataStoreProvider 
provider, final ParameterValueGroup parameters)
-                throws IllegalOpenParameterException
-        {
-            ParameterNotFoundException cause = null;
-            if (parameters != null) try {
-                final Object location = 
parameters.parameter(LOCATION).getValue();
-                if (location != null) {
-                    return new StorageConnector(location);
-                }
-            } catch (ParameterNotFoundException e) {
-                cause = e;
-            }
-            throw new 
IllegalOpenParameterException(Resources.format(Resources.Keys.UndefinedParameter_2,
-                        provider.getShortName(), LOCATION), cause);
+    /**
+     * Reads a XML document from an input stream using JAXB. The {@link 
#dataLocale} and {@link #timezone}
+     * are specified to the unmarshaller. Warnings are redirected to the 
listeners.
+     *
+     * @param  input   the input stream of the XML document. Will be closed by 
this method.
+     * @param  source  the source of the XML document to read.
+     * @return the unmarshalled object.
+     * @throws JAXBException if an error occurred while parsing the XML 
document.
+     * @throws IOException if an error occurred while closing the input stream.
+     */
+    protected final Object readXML(final InputStream input, final URI source) 
throws IOException, JAXBException {
+        try (input) {
+            return readXML(URISource.create(input, source));
         }
+    }
 
-        /**
-         * Returns {@code true} if the open options contains {@link 
StandardOpenOption#WRITE}
-         * or if the storage type is some kind of output stream. An ambiguity 
may exist between
-         * the case when a new file would be created and when an existing file 
would be updated.
-         * This ambiguity is resolved by the {@code ifNew} argument:
-         * if {@code false}, then the two cases are not distinguished.
-         * If {@code true}, then this method returns {@code true} only if a 
new file would be created.
-         *
-         * @param  connector  the connector to use for opening a file.
-         * @param  ifNew  whether to return {@code true} only if a new file 
would be created.
-         * @return whether the specified connector should open a writable data 
store.
-         * @throws DataStoreException if the storage object has already been 
used and cannot be reused.
-         */
-        public static boolean isWritable(final StorageConnector connector, 
final boolean ifNew) throws DataStoreException {
-            final Object storage = connector.getStorage();
-            if (storage instanceof OutputStream || storage instanceof 
DataOutput) return true;    // Must be tested first.
-            if (storage instanceof InputStream  || storage instanceof 
DataInput)  return false;   // Ignore options.
-            final OpenOption[] options = 
connector.getOption(OptionKey.OPEN_OPTIONS);
-            if (ArraysExt.contains(options, StandardOpenOption.WRITE)) {
-                if (!ifNew || ArraysExt.contains(options, 
StandardOpenOption.TRUNCATE_EXISTING)) {
-                    return true;
-                }
-                if (ArraysExt.contains(options, 
StandardOpenOption.CREATE_NEW)) {
-                    return IOUtilities.isKindOfPath(storage);
-                }
-                if (ArraysExt.contains(options, StandardOpenOption.CREATE)) {
-                    final Path path = connector.getStorageAs(Path.class);
-                    return (path != null) && Files.notExists(path);
-                }
-            }
-            return false;
-        }
+    /**
+     * Reads a XML document from a source using JAXB. The {@link #dataLocale} 
and {@link #timezone}
+     * are specified to the unmarshaller. Warnings are redirected to the 
listeners.
+     *
+     * @param  source  the source of the XML document to read.
+     * @return the unmarshalled object.
+     * @throws JAXBException if an error occurred while parsing the XML 
document.
+     */
+    protected final Object readXML(final Source source) throws JAXBException {
+        java.util.logging.Filter handler = (record) -> {
+            record.setLoggerName(null);        // For allowing `listeners` to 
use the provider's logger name.
+            listeners.warning(record);
+            return true;
+        };
+        // Cannot use Map.of(…) because it does not accept null values.
+        Map<String,Object> properties = new HashMap<>(8);
+        properties.put(XML.LOCALE, dataLocale);
+        properties.put(XML.TIMEZONE, timezone);
+        properties.put(XML.WARNING_FILTER, handler);
+        return XML.unmarshal(source, properties);
     }
 
     /**
-     * Returns the location (path, URL, URI, <i>etc.</i>) of the given 
resource.
-     * The type of the returned object can be any of the types documented in 
{@link DataStoreProvider#LOCATION}.
-     * The main ones are {@link java.net.URI}, {@link java.nio.file.Path} and 
JDBC {@linkplain javax.sql.DataSource}.
+     * Reads the content of the auxiliary file with the specified extension.
+     * This method uses the {@link #location} URI with the extension replaced 
by the given value.
+     * The file content is read and stored as a character sequence decoded 
according the store
+     * {@linkplain #encoding}, unless {@code acceptXML} is {@code true} and 
the file has been
+     * identified as an XML file. In the latter case, the character sequence 
is empty and the
+     * source must be read with {@link AuxiliaryContent#source}.
      *
-     * @param  resource  the resource for which to get the location, or {@code 
null}.
-     * @return location of the given resource, or {@code null} if none.
-     * @throws DataStoreException if an error on the file system prevent the 
creation of the path.
+     * <h4>Limitations</h4>
+     * This method is suitable for reasonably small files. An arbitrary size 
limit is applied for safety,
+     * unless {@code acceptXML} is {@code true} and the file has been detected 
as an XML file.
+     *
+     * @param  extension  the filename extension (without leading dot) of the 
auxiliary file to open,
+     *                    or {@code null} for using the main file without 
changing its extension.
+     * @param  acceptXML  whether to check if the source is a XML file.
+     * @return the file content together with the source, or {@code null} if 
none. Should be short-lived.
+     * @throws NoSuchFileException if the auxiliary file has not been found 
(when opened from path).
+     * @throws FileNotFoundException if the auxiliary file has not been found 
(when opened from URL).
+     * @throws IOException if another error occurred while opening the stream.
+     * @throws DataStoreException if the auxiliary file content seems too 
large.
      */
-    public static Object location(final Resource resource) throws 
DataStoreException {
-        if (resource instanceof DataStore) {
-            final Optional<ParameterValueGroup> p = ((DataStore) 
resource).getOpenParameters();
-            if (p.isPresent()) try {
-                return 
p.get().parameter(DataStoreProvider.LOCATION).getValue();
-            } catch (ParameterNotFoundException e) {
-                /*
-                 * This exception should not happen often since the "location" 
parameter is recommended.
-                 * Note that it does not mean the same thing as "parameter 
provided but value is null".
-                 * In that later case we want to return the null value as 
specified in the parameters.
-                 */
-                Logging.recoverableException(StoreUtilities.LOGGER, 
URIDataStore.class, "location", e);
+    protected final AuxiliaryContent readAuxiliaryFile(final String extension, 
final boolean acceptXML)
+            throws IOException, DataStoreException
+    {
+        /*
+         * Try to open the stream using the storage type (Path or URL) closest 
to the type
+         * given at construction time. We do that because those two types 
cannot open the
+         * same streams. For example, Path does not open HTTP or FTP 
connections by default,
+         * and URL does not open S3 files in current implementation.
+         */
+        final InputStream stream;
+        Path path = locationAsPath;
+        final Object source;                    // In case an error message is 
produced.
+        final URI sourceURI;                    // The source as an URI, or 
null.
+        if (path != null) {
+            if (extension != null) {
+                path = 
path.resolveSibling(getBaseFilename(path).concat(extension));
+            }
+            stream    = open(path);
+            source    = path;
+            sourceURI = null;
+        } else try {
+            sourceURI = (extension != null) ? 
IOUtilities.toAuxiliaryURI(location, extension, true) : location;
+            if (sourceURI == null) {
+                return null;
             }
+            final URL url = sourceURI.toURL();
+            stream = url.openStream();
+            source = url;
+        } catch (URISyntaxException e) {
+            throw new DataStoreException(cannotReadAuxiliaryFile(extension), 
e);
         }
         /*
-         * This fallback should not happen with `URIDataStore` implementation 
because the "location" parameter
-         * is always present even if null. This fallback is for resources 
implementated by different classes.
+         * If enabled, tests if the file is an XML file. If this is the case, 
we need to use `URISource`
+         * for giving a chance of `org.apache.sis.xml.XML.unmarshal(Source)` 
to resolve relative links.
          */
-        if (resource instanceof ResourceOnFileSystem) {
-            final Path[] paths = ((ResourceOnFileSystem) 
resource).getComponentFiles();
-            if (paths != null && paths.length != 0) {
-                return paths[0];                                    // First 
path is presumed the main file.
-            }
+        if (acceptXML && stream.markSupported() && 
org.apache.sis.storage.xml.AbstractProvider.isXML(stream)) {
+            return new AuxiliaryContent(source, URISource.create(stream, (path 
!= null) ? path.toUri() : sourceURI));
         }
-        return null;
+        /*
+         * If the auxiliary file is not an XML file, reads it fully as a text 
file with an arbitrary size limit.
+         */
+        var content = AuxiliaryContent.read(source, stream, encoding);
+        if (content != null) {
+            return content;
+        }
+        throw new DataStoreContentException(Resources.forLocale(getLocale())
+                .getString(Resources.Keys.AuxiliaryFileTooLarge_1, 
IOUtilities.filename(source)));
     }
 
     /**
-     * Adds the filename (without extension) as the citation title if there is 
no title, or as the identifier otherwise.
-     * This method should be invoked last, after {@code DataStore} 
implementation did its best effort for adding a title.
-     * The intend is actually to provide an identifier, but since the title is 
mandatory in ISO 19115 metadata,
-     * providing only an identifier without title would be invalid.
+     * Creates a writer for an auxiliary file with the specified extension.
+     * This method uses the same path as {@link #location},
+     * except for the extension which is replaced by the given value.
      *
-     * @param  builder  where to add the title or identifier.
+     * @param  extension  the filename extension of the auxiliary file to 
write.
+     * @return a stream opened on the specified file.
+     * @throws DataStoreException if the auxiliary file cannot be created.
+     * @throws IOException if another error occurred while opening the stream.
      */
-    protected final void addTitleOrIdentifier(final MetadataBuilder builder) {
-        final String filename = getFilename();
-        if (filename != null) {
-            builder.addTitleOrIdentifier(filename, MetadataBuilder.Scope.ALL);
+    protected final BufferedWriter writeAuxiliaryFile(final String extension) 
throws IOException, DataStoreException {
+        Path path = locationAsPath;
+        if (path == null) {
+            throw new ReadOnlyStorageException(Resources.forLocale(getLocale())
+                    .getString(Resources.Keys.CanNotWriteResource_1, 
getDisplayName()));
         }
+        path = path.resolveSibling(getBaseFilename(path).concat(extension));
+        return (encoding != null) ? Files.newBufferedWriter(path, encoding)
+                                  : Files.newBufferedWriter(path);
     }
 
     /**
-     * If an auxiliary metadata file has been specified, merge that file to 
the given metadata.
-     * This step should be done only after the data store added its own 
metadata.
-     * Failure to load auxiliary metadata are only a warning.
+     * Deletes the auxiliary file with the given extension if it exists.
+     * If the auxiliary file does not exist, then this method does nothing.
      *
-     * @param  builder  where to merge the metadata.
+     * @param  extension  the filename extension of the auxiliary file to 
delete.
+     * @throws DataStoreException if the auxiliary file is not on a supported 
file system.
+     * @throws IOException if an error occurred while deleting the file.
      */
-    protected final void mergeAuxiliaryMetadata(final MetadataBuilder builder) 
{
-        Object metadata = null;
-        Exception error = null;
-        try {
-            final Path path = getMetadataPath();
-            if (path != null) {
-                metadata = XML.unmarshal(path);
-            } else {
-                final URI uri = getMetadataURI();
-                if (uri != null) {
-                    metadata = XML.unmarshal(uri.toURL());
-                }
+    protected final void deleteAuxiliaryFile(final String extension) throws 
DataStoreException, IOException {
+        String previous = null;
+        for (Path path : getComponentFiles()) {
+            final String base = getBaseFilename(path);
+            if (!base.equals(previous)) {
+                previous = base;
+                path = path.resolveSibling(base.concat(extension));
+                Files.deleteIfExists(path);
             }
-        } catch (URISyntaxException | IOException e) {
-            error = e;
-        } catch (JAXBException e) {
-            final Throwable cause = e.getCause();
-            error = (cause instanceof IOException) ? (Exception) cause : e;
-        }
-        if (metadata != null) {
-            builder.mergeMetadata(metadata, getLocale());
-        } else if (error != null) {
-            listeners.warning(cannotReadAuxiliaryFile("xml"), error);
         }
     }
 
     /**
-     * {@return the error message for saying than auxiliary file cannot be 
read}.
+     * Returns the filename of the given path without the file suffix.
+     * The returned string always ends in {@code '.'}, making it ready
+     * for concatenation of a new suffix.
+     */
+    static String getBaseFilename(final Path path) {
+        final String base = path.getFileName().toString();
+        final int s = base.lastIndexOf(IOUtilities.EXTENSION_SEPARATOR);
+        return (s >= 0) ? base.substring(0, s+1) : base + 
IOUtilities.EXTENSION_SEPARATOR;
+    }
+
+    /**
+     * {@return the error message for saying that an auxiliary file cannot be 
read}.
      *
-     * @param  extension  file extension of the auxiliary file, without 
leading dot.
+     * @param  extension  file extension (without leading dot) of the 
auxiliary file, or null for the main file.
      */
-    protected final String cannotReadAuxiliaryFile(final String extension) {
+    protected final String cannotReadAuxiliaryFile(String extension) {
+        if (extension == null) {
+            extension = IOUtilities.extension(location);
+        }
         return 
Resources.forLocale(getLocale()).getString(Resources.Keys.CanNotReadAuxiliaryFile_1,
 extension);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
new file mode 100644
index 0000000000..f70df8e68e
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/URIDataStoreProvider.java
@@ -0,0 +1,236 @@
+/*
+ * 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.storage.base;
+
+import java.util.Optional;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.StandardOpenOption;
+import java.nio.charset.Charset;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.parameter.ParameterNotFoundException;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreProvider;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.IllegalOpenParameterException;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.internal.Resources;
+import org.apache.sis.io.stream.IOUtilities;
+import org.apache.sis.setup.OptionKey;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.logging.Logging;
+
+
+/**
+ * Provider for {@link URIDataStore} instances.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public abstract class URIDataStoreProvider extends DataStoreProvider {
+    /**
+     * Description of the {@value #LOCATION} parameter.
+     */
+    public static final ParameterDescriptor<URI> LOCATION_PARAM;
+
+    /**
+     * Description of the "metadata" parameter.
+     */
+    public static final ParameterDescriptor<Path> METADATA_PARAM;
+
+    /**
+     * Description of the optional {@value #CREATE} parameter, which may be 
present in writable data store.
+     * This parameter is not included in the descriptor created by {@link 
#build(ParameterBuilder)} default
+     * implementation. It is subclass responsibility to add it if desired, 
only if supported.
+     */
+    public static final ParameterDescriptor<Boolean> CREATE_PARAM;
+
+    /**
+     * Description of the optional parameter for character encoding used by 
the data store.
+     * This parameter is not included in the descriptor created by {@link 
#build(ParameterBuilder)}
+     * default implementation. It is subclass responsibility to add it if 
desired.
+     */
+    public static final ParameterDescriptor<Charset> ENCODING;
+    static {
+        final ParameterBuilder builder = new ParameterBuilder();
+        ENCODING       = 
builder.addName("encoding").setDescription(Resources.formatInternational(Resources.Keys.DataStoreEncoding)).create(Charset.class,
 null);
+        CREATE_PARAM   = builder.addName( CREATE   
).setDescription(Resources.formatInternational(Resources.Keys.DataStoreCreate  
)).create(Boolean.class, null);
+        METADATA_PARAM = 
builder.addName("metadata").setDescription(Resources.formatInternational(Resources.Keys.MetadataLocation
 )).create(Path.class, null);
+        LOCATION_PARAM = builder.addName( LOCATION 
).setDescription(Resources.formatInternational(Resources.Keys.DataStoreLocation)).setRequired(true).create(URI.class,
 null);
+    }
+
+    /**
+     * The parameter descriptor to be returned by {@link #getOpenParameters()}.
+     * Created when first needed.
+     */
+    private volatile ParameterDescriptorGroup openDescriptor;
+
+    /**
+     * Creates a new provider.
+     */
+    protected URIDataStoreProvider() {
+    }
+
+    /**
+     * Returns a description of all parameters accepted by this provider for 
opening a data store.
+     * This method creates the descriptor only when first needed. Subclasses 
can override the
+     * {@link #build(ParameterBuilder)} method if they need to modify the 
descriptor to create.
+     *
+     * @return description of the parameters required or accepted for opening 
a {@link DataStore}.
+     */
+    @Override
+    public final ParameterDescriptorGroup getOpenParameters() {
+        ParameterDescriptorGroup desc = openDescriptor;
+        if (desc == null) {
+            openDescriptor = desc = build(new 
ParameterBuilder().addName(getShortName()));
+        }
+        return desc;
+    }
+
+    /**
+     * Invoked by {@link #getOpenParameters()} the first time that a parameter 
descriptor needs to be created.
+     * When invoked, the parameter group name is set to a name derived from 
the {@link #getShortName()} value.
+     * The default implementation creates a group containing {@link 
#LOCATION_PARAM} and {@link #METADATA_PARAM}.
+     * Subclasses can override if they need to create a group with more 
parameters.
+     *
+     * @param  builder  the builder to use for creating parameter descriptor. 
The group name is already set.
+     * @return the parameters descriptor created from the given builder.
+     */
+    protected ParameterDescriptorGroup build(final ParameterBuilder builder) {
+        return builder.createGroup(LOCATION_PARAM, METADATA_PARAM);
+    }
+
+    /**
+     * Convenience method creating a parameter descriptor containing only 
{@link #LOCATION_PARAM}.
+     * This convenience method is used for public providers that cannot extend 
this
+     * {@code URIDataStoreProvider} class because it is internal.
+     *
+     * @param  name  short name of the data store format.
+     * @return the descriptor for open parameters.
+     *
+     * @todo Verify if non-exported classes in JDK9 are hidden from Javadoc, 
like package-private classes.
+     *       If true, we could remove this hack and extend {@code 
URIDataStore} even in public classes.
+     */
+    public static ParameterDescriptorGroup descriptor(final String name) {
+        return new 
ParameterBuilder().addName(name).createGroup(LOCATION_PARAM);
+    }
+
+    /**
+     * Returns the location (path, URL, URI, <i>etc.</i>) of the given 
resource.
+     * The type of the returned object can be any of the types documented in 
{@link DataStoreProvider#LOCATION}.
+     *
+     * @param  resource  the resource for which to get the location, or {@code 
null}.
+     * @return location of the given resource, or {@code null} if none.
+     * @throws DataStoreException if an error on the file system prevent the 
creation of the path.
+     */
+    public static Object location(final Resource resource) throws 
DataStoreException {
+        if (resource instanceof DataStore) {
+            final Optional<ParameterValueGroup> p = ((DataStore) 
resource).getOpenParameters();
+            if (p.isPresent()) try {
+                return 
p.get().parameter(DataStoreProvider.LOCATION).getValue();
+            } catch (ParameterNotFoundException e) {
+                /*
+                 * This exception should not happen often since the "location" 
parameter is recommended.
+                 * Note that it does not mean the same thing as "parameter 
provided but value is null".
+                 * In that later case we want to return the null value as 
specified in the parameters.
+                 */
+                Logging.recoverableException(StoreUtilities.LOGGER, 
URIDataStore.class, "location", e);
+            }
+        }
+        /*
+         * This fallback should not happen with `URIDataStore` implementation 
because the "location" parameter
+         * is always present even if null. This fallback is for resources 
implementated by different classes.
+         */
+        if (resource instanceof ResourceOnFileSystem) {
+            final Path[] paths = ((ResourceOnFileSystem) 
resource).getComponentFiles();
+            if (paths != null && paths.length != 0) {
+                return paths[0];                                    // First 
path is presumed the main file.
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Creates a storage connector initialized to the location declared in 
given parameters.
+     * This convenience method does not set any other parameters.
+     * In particular, reading (or ignoring) the {@value #CREATE} parameter is 
left to callers,
+     * because not all implementations may create data stores with {@link 
java.nio.file.StandardOpenOption}.
+     *
+     * @param  provider    the provider for which to create a storage 
connector (for error messages).
+     * @param  parameters  the parameters to use for creating a storage 
connector.
+     * @return the storage connector initialized to the location specified in 
the parameters.
+     * @throws IllegalOpenParameterException if no {@value #LOCATION} 
parameter has been found.
+     */
+    public static StorageConnector connector(final DataStoreProvider provider, 
final ParameterValueGroup parameters)
+            throws IllegalOpenParameterException
+    {
+        ParameterNotFoundException cause = null;
+        if (parameters != null) try {
+            final Object location = parameters.parameter(LOCATION).getValue();
+            if (location != null) {
+                return new StorageConnector(location);
+            }
+        } catch (ParameterNotFoundException e) {
+            cause = e;
+        }
+        throw new 
IllegalOpenParameterException(Resources.format(Resources.Keys.UndefinedParameter_2,
+                    provider.getShortName(), LOCATION), cause);
+    }
+
+    /**
+     * Returns {@code true} if the open options contains {@link 
StandardOpenOption#WRITE}
+     * or if the storage type is some kind of output stream. An ambiguity may 
exist between
+     * the case when a new file would be created and when an existing file 
would be updated.
+     * This ambiguity is resolved by the {@code ifNew} argument:
+     * if {@code false}, then the two cases are not distinguished.
+     * If {@code true}, then this method returns {@code true} only if a new 
file would be created.
+     *
+     * @param  connector  the connector to use for opening a file.
+     * @param  ifNew  whether to return {@code true} only if a new file would 
be created.
+     * @return whether the specified connector should open a writable data 
store.
+     * @throws DataStoreException if the storage object has already been used 
and cannot be reused.
+     */
+    public static boolean isWritable(final StorageConnector connector, final 
boolean ifNew) throws DataStoreException {
+        final Object storage = connector.getStorage();
+        if (storage instanceof OutputStream || storage instanceof DataOutput) 
return true;    // Must be tested first.
+        if (storage instanceof InputStream  || storage instanceof DataInput)  
return false;   // Ignore options.
+        final OpenOption[] options = 
connector.getOption(OptionKey.OPEN_OPTIONS);
+        if (ArraysExt.contains(options, StandardOpenOption.WRITE)) {
+            if (!ifNew || ArraysExt.contains(options, 
StandardOpenOption.TRUNCATE_EXISTING)) {
+                return true;
+            }
+            if (ArraysExt.contains(options, StandardOpenOption.CREATE_NEW)) {
+                return IOUtilities.isKindOfPath(storage);
+            }
+            if (ArraysExt.contains(options, StandardOpenOption.CREATE)) {
+                final Path path = connector.getStorageAs(Path.class);
+                return (path != null) && Files.notExists(path);
+            }
+        }
+        return false;
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
index 42f93fe314..f977152722 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/Store.java
@@ -643,7 +643,7 @@ final class Store extends URIDataStore implements 
FeatureSet {
             builder.addExtent(envelope, listeners);
             builder.addFeatureType(featureType, -1);
             mergeAuxiliaryMetadata(builder);
-            addTitleOrIdentifier(builder);
+            builder.addTitleOrIdentifier(getFilename(), 
MetadataBuilder.Scope.ALL);
             builder.setISOStandards(false);
             metadata = builder.buildAndFreeze();
         }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/StoreProvider.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/StoreProvider.java
index 56fcbbcf56..c66fc677ac 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/StoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/csv/StoreProvider.java
@@ -32,7 +32,7 @@ import org.apache.sis.feature.FoliationRepresentation;
 import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.storage.base.Capability;
 import org.apache.sis.storage.base.StoreMetadata;
-import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.storage.wkt.FirstKeywordPeek;
 import org.apache.sis.util.ArgumentChecks;
 
@@ -51,7 +51,7 @@ import org.apache.sis.util.ArgumentChecks;
                fileSuffixes  = "csv",
                capabilities  = Capability.READ,
                resourceTypes = FeatureSet.class)
-public final class StoreProvider extends URIDataStore.Provider {
+public final class StoreProvider extends URIDataStoreProvider {
     /**
      * The format names for static features and moving features.
      */
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
index 9743c7dad7..e8b41b97f5 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RasterStore.java
@@ -172,7 +172,7 @@ abstract class RasterStore extends PRJDataStore implements 
GridCoverageResource
             }
         }
         mergeAuxiliaryMetadata(builder);
-        addTitleOrIdentifier(builder);
+        builder.addTitleOrIdentifier(getFilename(), MetadataBuilder.Scope.ALL);
         builder.setISOStandards(false);
         metadata = builder.buildAndFreeze();
     }
@@ -209,7 +209,7 @@ abstract class RasterStore extends PRJDataStore implements 
GridCoverageResource
         }
         int count = 0;
         long[] indexAndColors = ArraysExt.EMPTY_LONG;       // Index in 
highest 32 bits, ARGB in lowest 32 bits.
-        for (final CharSequence line : 
CharSequences.splitOnEOL(readAuxiliaryFile(CLR))) {
+        for (final CharSequence line : 
CharSequences.splitOnEOL(readAuxiliaryFile(CLR, false))) {
             final int end   = CharSequences.skipTrailingWhitespaces(line, 0, 
line.length());
             final int start = CharSequences.skipLeadingWhitespaces(line, 0, 
end);
             if (start < end && Character.isDigit(Character.codePointAt(line, 
start))) {
@@ -280,7 +280,7 @@ abstract class RasterStore extends PRJDataStore implements 
GridCoverageResource
             throws DataStoreException, IOException
     {
         final Statistics[] stats = new Statistics[sm.getNumBands()];
-        for (final CharSequence line : 
CharSequences.splitOnEOL(readAuxiliaryFile(STX))) {
+        for (final CharSequence line : 
CharSequences.splitOnEOL(readAuxiliaryFile(STX, false))) {
             final int end   = CharSequences.skipTrailingWhitespaces(line, 0, 
line.length());
             final int start = CharSequences.skipLeadingWhitespaces(line, 0, 
end);
             if (start < end && Character.isDigit(Character.codePointAt(line, 
start))) {
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
index ddfa2321c0..8296557b59 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/esri/RawRasterStore.java
@@ -41,6 +41,7 @@ import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.DataStoreClosedException;
 import org.apache.sis.storage.DataStoreContentException;
 import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.base.AuxiliaryContent;
 import org.apache.sis.storage.internal.Resources;
 import org.apache.sis.io.stream.ChannelDataInput;
 import org.apache.sis.referencing.util.j2d.AffineTransform2D;
@@ -241,6 +242,7 @@ final class RawRasterStore extends RasterStore {
      */
     @Override
     public synchronized List<SampleDimension> getSampleDimensions() throws 
DataStoreException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final ChannelDataInput input = this.input;
         List<SampleDimension> sampleDimensions = super.getSampleDimensions();
         if (sampleDimensions == null) try {
@@ -335,6 +337,7 @@ final class RawRasterStore extends RasterStore {
      */
     private void readHeader() throws IOException, DataStoreException {
         assert Thread.holdsLock(this);
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final ChannelDataInput input = this.input;
         if (input == null) {
             throw new DataStoreClosedException(canNotRead());
@@ -355,7 +358,7 @@ final class RawRasterStore extends RasterStore {
         int     geomask        = 0;   // Mask telling whether ulxmap, ulymap, 
xdim, ydim were specified (in that order).
         RawRasterLayout layout = RawRasterLayout.BIL;
         ByteOrder byteOrder    = ByteOrder.nativeOrder();
-        final AuxiliaryContent header = 
readAuxiliaryFile(RawRasterStoreProvider.HDR);
+        final AuxiliaryContent header = 
readAuxiliaryFile(RawRasterStoreProvider.HDR, false);
         if (header == null) {
             throw new 
DataStoreException(cannotReadAuxiliaryFile(RawRasterStoreProvider.HDR));
         }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/StoreProvider.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/StoreProvider.java
index 556a72567c..cc26a51f10 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/StoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/folder/StoreProvider.java
@@ -45,7 +45,7 @@ import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.storage.internal.Resources;
-import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.storage.base.Capability;
 import org.apache.sis.storage.base.StoreMetadata;
 import org.apache.sis.storage.base.StoreUtilities;
@@ -106,12 +106,12 @@ public final class StoreProvider extends 
DataStoreProvider {
         final ParameterDescriptor<Path> location;
         final ParameterBuilder builder = new ParameterBuilder();
         final InternationalString remark = 
Resources.formatInternational(Resources.Keys.UsedOnlyIfNotEncoded);
-        ENCODING   = annotate(builder, URIDataStore.Provider.ENCODING, remark);
+        ENCODING   = annotate(builder, URIDataStoreProvider.ENCODING, remark);
         LOCALE     = builder.addName("locale"  
).setDescription(Resources.formatInternational(Resources.Keys.DataStoreLocale  
)).setRemarks(remark).create(Locale.class,   null);
         TIMEZONE   = 
builder.addName("timezone").setDescription(Resources.formatInternational(Resources.Keys.DataStoreTimeZone)).setRemarks(remark).create(TimeZone.class,
 null);
         FORMAT     = builder.addName("format"  
).setDescription(Resources.formatInternational(Resources.Keys.DirectoryContentFormatName)).create(String.class,
 null);
-        location   = new 
ParameterBuilder(URIDataStore.Provider.LOCATION_PARAM).create(Path.class, null);
-        PARAMETERS = builder.addName(NAME).createGroup(location, LOCALE, 
TIMEZONE, ENCODING, FORMAT, URIDataStore.Provider.CREATE_PARAM);
+        location   = new 
ParameterBuilder(URIDataStoreProvider.LOCATION_PARAM).create(Path.class, null);
+        PARAMETERS = builder.addName(NAME).createGroup(location, LOCALE, 
TIMEZONE, ENCODING, FORMAT, URIDataStoreProvider.CREATE_PARAM);
     }
 
     /**
@@ -276,13 +276,13 @@ public final class StoreProvider extends 
DataStoreProvider {
     @Override
     public DataStore open(final ParameterValueGroup parameters) throws 
DataStoreException {
         ArgumentChecks.ensureNonNull("parameter", parameters);
-        final StorageConnector connector = 
URIDataStore.Provider.connector(this, parameters);
+        final StorageConnector connector = 
URIDataStoreProvider.connector(this, parameters);
         final Parameters pg = Parameters.castOrWrap(parameters);
         connector.setOption(OptionKey.LOCALE,   pg.getValue(LOCALE));
         connector.setOption(OptionKey.TIMEZONE, pg.getValue(TIMEZONE));
         connector.setOption(OptionKey.ENCODING, pg.getValue(ENCODING));
         final EnumSet<StandardOpenOption> options = 
EnumSet.of(StandardOpenOption.WRITE);
-        if 
(Boolean.TRUE.equals(pg.getValue(URIDataStore.Provider.CREATE_PARAM))) {
+        if 
(Boolean.TRUE.equals(pg.getValue(URIDataStoreProvider.CREATE_PARAM))) {
             options.add(StandardOpenOption.CREATE);
         }
         return open(connector, pg.getValue(FORMAT), options);
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
index 9daf3e10cf..bf06e72815 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java
@@ -50,6 +50,7 @@ import org.apache.sis.storage.ReadOnlyStorageException;
 import org.apache.sis.storage.UnsupportedStorageException;
 import org.apache.sis.storage.base.PRJDataStore;
 import org.apache.sis.storage.base.MetadataBuilder;
+import org.apache.sis.storage.base.AuxiliaryContent;
 import org.apache.sis.referencing.util.j2d.AffineTransform2D;
 import org.apache.sis.metadata.sql.MetadataStoreException;
 import org.apache.sis.util.CharSequences;
@@ -282,6 +283,7 @@ public class WorldFileStore extends PRJDataStore {
      * does not support the locale, the reader's default locale will be used.
      */
     private void configureReader() {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final ImageReader reader = this.reader;
         try {
             reader.setLocale(listeners.getLocale());
@@ -371,7 +373,7 @@ loop:   for (int convention=0;; convention++) {
      * @throws DataStoreException if the file content cannot be parsed.
      */
     private AffineTransform2D readWorldFile(final String wld) throws 
IOException, DataStoreException {
-        final AuxiliaryContent content = readAuxiliaryFile(wld);
+        final AuxiliaryContent content = readAuxiliaryFile(wld, false);
         if (content == null) {
             listeners.warning(cannotReadAuxiliaryFile(wld));
             return null;
@@ -422,11 +424,12 @@ loop:   for (int convention=0;; convention++) {
      * @return the requested names, or an empty array if none or unknown.
      */
     public String[] getImageFormat(final boolean asMimeType) {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final ImageReader reader = this.reader;
         if (reader != null) {
-            final ImageReaderSpi provider = reader.getOriginatingProvider();
-            if (provider != null) {
-                final String[] names = asMimeType ? provider.getMIMETypes() : 
provider.getFormatNames();
+            final ImageReaderSpi p = reader.getOriginatingProvider();
+            if (p != null) {
+                final String[] names = asMimeType ? p.getMIMETypes() : 
p.getFormatNames();
                 if (names != null) {
                     return names;
                 }
@@ -463,6 +466,7 @@ loop:   for (int convention=0;; convention++) {
      */
     final GridGeometry getGridGeometry(final int index) throws IOException, 
DataStoreException {
         assert Thread.holdsLock(this);
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final ImageReader reader = reader();
         if (gridGeometry == null) {
             final AffineTransform2D gridToCRS;
@@ -533,7 +537,7 @@ loop:   for (int convention=0;; convention++) {
                 builder.addExtent(gridGeometry.getEnvelope(), listeners);
             }
             mergeAuxiliaryMetadata(builder);
-            addTitleOrIdentifier(builder);
+            builder.addTitleOrIdentifier(getFilename(), 
MetadataBuilder.Scope.ALL);
             builder.setISOStandards(false);
             metadata = builder.buildAndFreeze();
         } catch (IOException e) {
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
index 1e7d36088b..39f32775a0 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java
@@ -85,7 +85,7 @@ public class Resources extends IndexedResourceBundle {
         public static final short CanNotIntersectDataWithQuery_1 = 57;
 
         /**
-         * Cannot read “{0}” auxiliary file.
+         * Cannot read the “{0}” auxiliary file.
          */
         public static final short CanNotReadAuxiliaryFile_1 = 66;
 
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
index 6356fb4950..4e433e2b72 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties
@@ -25,7 +25,7 @@ CanNotCreateFolderStore_1         = Cannot create resources 
based on the content
 CanNotDeriveTypeFromFeature_1     = Cannot infer the feature type resulting 
from \u201c{0}\u201d filtering.
 CanNotGetCommonMetadata_2         = Cannot get metadata common to 
\u201c{0}\u201d files. The reason is: {1}
 CanNotIntersectDataWithQuery_1    = Cannot intersect \u201c{0}\u201d data with 
specified query.
-CanNotReadAuxiliaryFile_1         = Cannot read \u201c{0}\u201d auxiliary file.
+CanNotReadAuxiliaryFile_1         = Cannot read the \u201c{0}\u201d auxiliary 
file.
 CanNotReadCRS_WKT_1               = Cannot read the Coordinate Reference 
System (CRS) Well Known Text (WKT) in \u201c{0}\u201d.
 CanNotReadDirectory_1             = Cannot read \u201c{0}\u201d directory.
 CanNotReadFile_2                  = Cannot read \u201c{1}\u201d as a file in 
the {0} format.
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/Store.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/Store.java
index 1e4a7099a0..acb4923da4 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/Store.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/Store.java
@@ -184,7 +184,7 @@ final class Store extends URIDataStore {
                 mergeAuxiliaryMetadata(builder);
             } else {
                 mergeAuxiliaryMetadata(builder);
-                addTitleOrIdentifier(builder);
+                builder.addTitleOrIdentifier(getFilename(), 
MetadataBuilder.Scope.ALL);
             }
             metadata = builder.buildAndFreeze();
         }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreProvider.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreProvider.java
index 8371627650..d95493de90 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/wkt/StoreProvider.java
@@ -26,7 +26,7 @@ import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.ProbeResult;
 import org.apache.sis.storage.base.Capability;
 import org.apache.sis.storage.base.StoreMetadata;
-import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.referencing.util.WKTKeywords;
 import org.apache.sis.util.Version;
 
@@ -39,7 +39,7 @@ import org.apache.sis.util.Version;
 @StoreMetadata(formatName   = StoreProvider.NAME,
                fileSuffixes = "prj",
                capabilities = Capability.READ)
-public final class StoreProvider extends URIDataStore.Provider {
+public final class StoreProvider extends URIDataStoreProvider {
     /**
      * The format name.
      */
@@ -68,7 +68,9 @@ public final class StoreProvider extends 
URIDataStore.Provider {
         static final int MIN_LENGTH = 6;
 
         /**
-         * The set of WKT keywords.
+         * The set of WKT keywords for CRS definitions.
+         * This set does not include the WKT keywords for coordinate 
operations,
+         * because the WKT store can only return metadata and metadata can 
only store the CRS.
          */
         private final Set<String> keywords;
 
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/AbstractProvider.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/AbstractProvider.java
index 4fd9176d97..56e3e53326 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/AbstractProvider.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/xml/AbstractProvider.java
@@ -19,6 +19,7 @@ package org.apache.sis.storage.xml;
 import java.util.Map;
 import java.io.Reader;
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.ByteBuffer;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
@@ -91,6 +92,25 @@ public abstract class AbstractProvider extends 
DocumentedStoreProvider {
         this.mimeForRootElements = mimeForRootElements;
     }
 
+    /**
+     * Returns {@code true} if the given input stream begins with the XML 
header.
+     * This method should be invoked only if mark and reset are supported.
+     *
+     * @param  in  the input stream to test.
+     * @return whether the first bytes are {@code "<?xml "}.
+     * @throws IOException if an error occurred while reading the input stream.
+     */
+    public static boolean isXML(final InputStream in) throws IOException {
+        boolean isXML = true;
+        in.mark(HEADER.length);
+        for (int i=0; i<HEADER.length; i++) {
+            isXML = (in.read() == HEADER[i]);
+            if (!isXML) break;
+        }
+        in.reset();
+        return isXML;
+    }
+
     /**
      * Returns the MIME type if the given storage appears to be supported by 
the data store.
      * A {@linkplain ProbeResult#isSupported() supported} status does not 
guarantee that reading
diff --git 
a/incubator/src/org.apache.sis.storage.coveragejson/main/org/apache/sis/storage/coveragejson/CoverageJsonStoreProvider.java
 
b/incubator/src/org.apache.sis.storage.coveragejson/main/org/apache/sis/storage/coveragejson/CoverageJsonStoreProvider.java
index 7e84195424..723c9e6e8a 100644
--- 
a/incubator/src/org.apache.sis.storage.coveragejson/main/org/apache/sis/storage/coveragejson/CoverageJsonStoreProvider.java
+++ 
b/incubator/src/org.apache.sis.storage.coveragejson/main/org/apache/sis/storage/coveragejson/CoverageJsonStoreProvider.java
@@ -28,7 +28,7 @@ import org.apache.sis.storage.ProbeResult;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.base.Capability;
 import org.apache.sis.storage.base.StoreMetadata;
-import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.util.Version;
 
 
@@ -65,7 +65,7 @@ public class CoverageJsonStoreProvider extends 
DataStoreProvider {
     /**
      * The parameter descriptor to be returned by {@link #getOpenParameters()}.
      */
-    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR = 
URIDataStore.Provider.descriptor(NAME);
+    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR = 
URIDataStoreProvider.descriptor(NAME);
 
     public CoverageJsonStoreProvider() {
     }
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java
index 81f00038ff..61a36be73e 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/PathAction.java
@@ -33,7 +33,7 @@ import org.apache.sis.gui.internal.ExceptionReporter;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.base.ResourceOnFileSystem;
-import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.io.stream.IOUtilities;
 
 
@@ -87,7 +87,7 @@ final class PathAction implements EventHandler<ActionEvent> {
         final Resource resource = cell.getItem();
         final Object path;
         try {
-            path = URIDataStore.location(resource);
+            path = URIDataStoreProvider.location(resource);
         } catch (DataStoreException e) {
             ExceptionReporter.show(cell, null, null, e);
             return;
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceCell.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceCell.java
index faee06a2f0..d7f1c7fe64 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceCell.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ResourceCell.java
@@ -27,7 +27,7 @@ import javafx.scene.control.TreeItem;
 import javafx.scene.paint.Color;
 import org.apache.sis.storage.Resource;
 import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.base.URIDataStore;
+import org.apache.sis.storage.base.URIDataStoreProvider;
 import org.apache.sis.io.stream.IOUtilities;
 import org.apache.sis.gui.internal.ExceptionReporter;
 import org.apache.sis.gui.internal.DataStoreOpener;
@@ -139,7 +139,7 @@ final class ResourceCell extends TreeCell<Resource> {
                  */
                 Object path;
                 try {
-                    path = URIDataStore.location(resource);
+                    path = URIDataStoreProvider.location(resource);
                 } catch (DataStoreException e) {
                     path = null;
                     ResourceTree.unexpectedException("updateItem", e);


Reply via email to