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 2514bc80b8 Replace the use of `java.net.URI` by an internal `GridFile` 
class which take in charge the URI resolutions. This is for making easier the 
cases where the URI needs to be resolved relatively to the GML or WKT file 
instead of resolved in the "$SIS_DATA/DatumChanges" directory.
2514bc80b8 is described below

commit 2514bc80b80d9987fbee054eac5aa4ed9af9c355
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Thu Dec 28 17:23:01 2023 +0100

    Replace the use of `java.net.URI` by an internal `GridFile` class which 
take in charge the URI resolutions.
    This is for making easier the cases where the URI needs to be resolved 
relatively to the GML or WKT file
    instead of resolved in the "$SIS_DATA/DatumChanges" directory.
---
 .../sis/parameter/DefaultParameterValue.java       |   1 +
 .../main/org/apache/sis/parameter/Parameters.java  |  24 ++
 .../referencing/operation/gridded/GridFile.java    | 290 +++++++++++++++++++++
 .../referencing/operation/gridded/GridGroup.java   |   5 +-
 .../referencing/operation/gridded/GridLoader.java  |  79 +-----
 .../referencing/operation/gridded/LoadedGrid.java  |  22 +-
 .../provider/FranceGeocentricInterpolation.java    |  35 +--
 .../sis/referencing/operation/provider/NADCON.java |  34 +--
 .../sis/referencing/operation/provider/NTv2.java   |  18 +-
 .../operation/provider/DatumShiftTestCase.java     |   5 +-
 .../FranceGeocentricInterpolationTest.java         |  13 +-
 .../referencing/operation/provider/NADCONTest.java |   7 +-
 .../referencing/operation/provider/NTv2Test.java   |  19 +-
 .../sis/test/integration/DatumShiftTest.java       |   5 +-
 .../main/org/apache/sis/system/DataDirectory.java  |  36 +--
 15 files changed, 402 insertions(+), 191 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterValue.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterValue.java
index 7cdc01b585..ce89c5b038 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterValue.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterValue.java
@@ -258,6 +258,7 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      *     }
      *
      * @see #setSourceFile(URI)
+     * @see Parameters#getSourceFile(ParameterDescriptor)
      * @see org.apache.sis.io.wkt.WKTFormat#getSourceFile()
      * @see org.apache.sis.xml.MarshalContext#getDocumentURI()
      * @see URI#resolve(URI)
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Parameters.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Parameters.java
index 7f62e5f7d0..c4257787bb 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Parameters.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/Parameters.java
@@ -19,7 +19,9 @@ package org.apache.sis.parameter;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.Predicate;
+import java.net.URI;
 import java.io.Serializable;
 import jakarta.xml.bind.annotation.XmlTransient;
 import javax.measure.Unit;
@@ -513,6 +515,28 @@ public abstract class Parameters implements 
ParameterValueGroup, Cloneable, Prin
         return false;
     }
 
+    /**
+     * {@return the URI of the GML document or WKT file from which a parameter 
value has been read}.
+     * This information can be used together with {@code 
getValue(ParameterDescriptor<URI>)} for
+     * resolving a parameter value as a path relative to the GML or WKT file 
declaring the parameter.
+     * Note that the source file is not necessarily the same for all 
parameters in a group, because a GML
+     * document could define parameters in files referenced by different 
{@code xlink:href} attribute values.
+     *
+     * @see DefaultParameterValue#getSourceFile()
+     * @see org.apache.sis.io.wkt.WKTFormat#getSourceFile()
+     * @see org.apache.sis.xml.MarshalContext#getDocumentURI()
+     * @see URI#resolve(URI)
+     *
+     * @since 1.5
+     */
+    public Optional<URI> getSourceFile(final ParameterDescriptor<?> parameter) 
throws ParameterNotFoundException {
+        final ParameterValue<?> p = getParameter(parameter);
+        if (p instanceof DefaultParameterValue<?>) {
+            return ((DefaultParameterValue<?>) p).getSourceFile();
+        }
+        return Optional.empty();
+    }
+
     /**
      * Returns the value of the parameter identified by the given descriptor, 
or {@code null} if none.
      * This method uses the following information from the given {@code 
parameter} descriptor:
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridFile.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridFile.java
new file mode 100644
index 0000000000..c6fc045fa4
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridFile.java
@@ -0,0 +1,290 @@
+/*
+ * 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.referencing.operation.gridded;
+
+import java.net.URI;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.channels.Channels;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.FileSystemNotFoundException;
+import java.util.logging.Level;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.opengis.util.FactoryException;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterNotFoundException;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.referencing.factory.FactoryDataException;
+import org.apache.sis.referencing.factory.MissingFactoryResourceException;
+import org.apache.sis.referencing.operation.provider.AbstractProvider;
+import org.apache.sis.referencing.internal.Resources;
+import org.apache.sis.system.DataDirectory;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.resources.Messages;
+
+
+/**
+ * Resolved path to a grid file. The starting point is the path specified by a 
parameter.
+ * If that path is relative, then this class tries to resolve it in a 
directory specified
+ * by the {@code SIS_DATA} environment variable. If the path cannot be 
resolved that way,
+ * then this method check if it can be resolved relatively to the GML or WKT 
file containing
+ * the parameter.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class GridFile {
+    /**
+     * Whether the tip about the location of datum shift files has been logged.
+     * We log this tip only once, and only if we failed to load at least one 
grid.
+     */
+    private static final AtomicBoolean datumDirectoryLogged = new 
AtomicBoolean();
+
+    /**
+     * The directory where to search for a local copy of the data, or {@code 
null} if none.
+     */
+    private final DataDirectory localDirectory;
+
+    /**
+     * The URI specified in the parameter. This URI is usually relative to an 
unspecified directory.
+     *
+     * @see #resolved()
+     */
+    public final URI parameter;
+
+    /**
+     * The URI as an absolute path.
+     */
+    private URI resolved;
+
+    /**
+     * The base URI used for resolving the parameter, or {@code null} if none.
+     */
+    private URI base;
+
+    /**
+     * The resolved URI as a path, or {@code null} if not yet computed or not 
convertible.
+     */
+    private Path asPath;
+
+    /**
+     * Creates a file for the given URI, assumed already resolved.
+     * This constructor is for testing purposes.
+     *
+     * @param  resolved  the resolved URI.
+     */
+    public GridFile(final URI resolved) {
+        parameter = resolved;
+        this.resolved = resolved;
+        localDirectory = DataDirectory.DATUM_CHANGES;
+    }
+
+    /**
+     * Resolves the given parameter as an absolute URI, resolved in the {@code 
"$SIS_DATA/DatumChanges"} directory
+     * if the URI is relative. If the URI cannot be resolved, a {@link 
MissingFactoryResourceException} is thrown.
+     * That exception type is necessary for letting the caller know that a 
coordinate operation is probably valid
+     * but cannot be constructed because an optional configuration is missing.
+     * It is typically because the {@code SIS_DATA} environment variable has 
not been set.
+     *
+     * @param  group  the group of parameters from which to get the URI.
+     * @param  param  identification of the parameter to fetch.
+     * @throws ParameterNotFoundException if the specified parameter is not 
found in the given group.
+     * @throws MissingFactoryResourceException if the path cannot be resolved.
+     */
+    public GridFile(final Parameters group, final ParameterDescriptor<URI> 
param) throws MissingFactoryResourceException {
+        this(group, param, DataDirectory.DATUM_CHANGES);
+    }
+
+    /**
+     * Resolves the given parameter as an absolute URI, resolved with the 
specified {@code DataDirectory}
+     * if the URI is relative. If the URI cannot be resolved, a {@link 
MissingFactoryResourceException} is thrown.
+     * That exception type is necessary for letting the caller know that a 
coordinate operation is probably valid
+     * but cannot be constructed because an optional configuration is missing.
+     * It is typically because the {@code SIS_DATA} environment variable has 
not been set.
+     *
+     * @param  group           the group of parameters from which to get the 
URI.
+     * @param  param           identification of the parameter to fetch.
+     * @param  localDirectory  the directory where to search for a local copy 
of the data, or {@code null} if none.
+     * @throws ParameterNotFoundException if the specified parameter is not 
found in the given group.
+     * @throws MissingFactoryResourceException if the path cannot be resolved.
+     */
+    public GridFile(final Parameters group, final ParameterDescriptor<URI> 
param, final DataDirectory localDirectory)
+            throws MissingFactoryResourceException
+    {
+        RuntimeException error = null;
+        this.localDirectory = localDirectory;
+        parameter = group.getMandatoryValue(param);
+        if (parameter.isAbsolute()) {
+            resolved = parameter.normalize();
+        } else {
+            /*
+             * First, try to resolve the parameter relative to the 
"$SIS_DATA/DatumChanges" directory.
+             * That directory can be seen as a cache to be tried before to 
download data that may be
+             * on the network.
+             */
+            if (localDirectory != null) {
+                base = localDirectory.getDirectoryAsURI();
+                if (base != null) try {
+                    resolved = base.resolve(parameter).normalize();
+                    asPath = Path.of(resolved);
+                    if (Files.exists(asPath)) {
+                        return;
+                    }
+                } catch (IllegalArgumentException | 
FileSystemNotFoundException e) {
+                    error = e;
+                }
+            }
+            /*
+             * If the "$SIS_DATA/DatumChanges" directory cannot be used, check 
if we
+             * have another base URI that we could try. If not, we cannot 
continue.
+             */
+            final URI document = group.getSourceFile(param).orElse(null);
+            if (document == null) {
+                if (resolved != null) {
+                    return;             // NoSuchFileException will be thrown 
later by `newByteChannel()`.
+                }
+                final String message;
+                if (parameter.isOpaque()) {
+                    message = Errors.format(Errors.Keys.CanNotOpen_1, 
parameter);
+                } else {
+                    final String env = DataDirectory.getenv();
+                    if (env == null) {
+                        message = 
Messages.format(Messages.Keys.DataDirectoryNotSpecified_1, DataDirectory.ENV);
+                    } else {
+                        message = 
Messages.format(Messages.Keys.DataDirectoryNotAccessible_2, DataDirectory.ENV, 
env);
+                    }
+                }
+                throw new MissingFactoryResourceException(message, error);
+            }
+            /*
+             * Use the alternative base URI without checking if it exists.
+             * This check will be done when the file will be opened.
+             */
+            base = document;
+            resolved = document.resolve(parameter).normalize();
+        }
+        try {
+            asPath = Path.of(resolved);
+        } catch (IllegalArgumentException | FileSystemNotFoundException e) {
+            if (error == null) error = e;
+            else error.addSuppressed(e);
+            asPath = null;
+        }
+        if (error != null) {
+            Logging.ignorableException(AbstractProvider.LOGGER, 
GridFile.class, "<init>", error);
+        }
+    }
+
+    /**
+     * {@return the resolved URI}.
+     *
+     * @see #parameter
+     */
+    public URI resolved() {
+        return resolved;
+    }
+
+    /**
+     * Creates a channel for reading bytes from the file at the path specified 
at construction time.
+     * This method tries to open using the file system before to open from the 
URL.
+     *
+     * @return a channel for reading bytes from the file.
+     * @throws IOException if the channel cannot be created.
+     */
+    public ReadableByteChannel newByteChannel() throws IOException {
+        if (asPath != null) {
+            return Files.newByteChannel(asPath);
+        } else {
+            return Channels.newChannel(resolved.toURL().openStream());
+        }
+    }
+
+    /**
+     * Creates a buffered reader for reading characters from the file at the 
path specified at construction time.
+     * This method tries to open using the file system before to open from the 
URL.
+     *
+     * @return a channel for reading bytes from the file.
+     * @throws IOException if the reader cannot be created.
+     */
+    public BufferedReader newBufferedReader() throws IOException {
+        if (asPath != null) {
+            return Files.newBufferedReader(asPath);
+        } else {
+            return new BufferedReader(new 
InputStreamReader(resolved.toURL().openStream()));
+        }
+    }
+
+    /**
+     * Logs a message about a grid which is about to be loaded.
+     * The logger will be {@code "org.apache.sis.referencing.operation"} and 
the originating
+     * method will be {@code "createMathTransform"} in the specified {@code 
caller} class.
+     *
+     * @param  caller  the provider to logs as the source class.
+     */
+    public void startLoading(final Class<?> caller) {
+        startLoading(caller, parameter);
+    }
+
+    /**
+     * Logs a message about a grid which is about to be loaded.
+     * The logger will be {@code "org.apache.sis.referencing.operation"} and 
the originating
+     * method will be {@code "createMathTransform"} in the specified {@code 
caller} class.
+     *
+     * @param  caller  the provider to logs as the source class.
+     * @param  file    the grid file, as a {@link String} or a {@link URI}.
+     */
+    public static void startLoading(final Class<?> caller, final Object file) {
+        GridLoader.log(caller, 
Resources.forLocale(null).getLogRecord(Level.FINE, 
Resources.Keys.LoadingDatumShiftFile_1, file));
+    }
+
+    /**
+     * Creates the exception to throw when the provider failed to load the 
grid file.
+     * The first time that this method is invoked, an information message is 
logged
+     * as a tip to the user about where data where searched.
+     *
+     * @param  caller  the provider to logs as the source class if a warning 
occurs.
+     * @param  format  the format name (e.g. "NTv2" or "NADCON").
+     * @param  cause   the cause of the failure to load the grid file.
+     */
+    public FactoryException canNotLoad(final Class<?> caller, final String 
format, final Exception cause) {
+        if (localDirectory != null && !datumDirectoryLogged.get()) {
+            final Path directory = localDirectory.getDirectory();
+            if (directory != null && !datumDirectoryLogged.getAndSet(true)) {
+                GridLoader.log(caller, 
Resources.forLocale(null).getLogRecord(Level.INFO,
+                                       Resources.Keys.DatumChangesDirectory_1, 
directory));
+            }
+        }
+        if (cause instanceof NoSuchFileException || cause instanceof 
FileNotFoundException) {
+            return new 
MissingFactoryResourceException(Resources.format(Resources.Keys.FileNotFound_2),
 cause);
+        } else {
+            return new 
FactoryDataException(Resources.format(Resources.Keys.FileNotReadable_2, format, 
parameter), cause);
+        }
+    }
+
+    /**
+     * {@return a string representation of this path for debugging purposes}.
+     */
+    @Override
+    public String toString() {
+        return String.valueOf(resolved);
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridGroup.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridGroup.java
index 2216277902..497eff3efc 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridGroup.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridGroup.java
@@ -19,7 +19,6 @@ package org.apache.sis.referencing.operation.gridded;
 import java.util.Map;
 import java.util.List;
 import java.util.LinkedHashMap;
-import java.net.URI;
 import java.io.IOException;
 import java.awt.Dimension;
 import java.awt.Rectangle;
@@ -156,7 +155,7 @@ public final class GridGroup<C extends Quantity<C>, T 
extends Quantity<T>> exten
      * @throws IOException declared because {@link Tile#getRegion()} declares 
it, but should not happen.
      */
     public static <C extends Quantity<C>, T extends Quantity<T>> 
GridGroup<C,T> create(
-            final URI file, final List<LoadedGrid<C,T>> subgrids)
+            final GridFile file, final List<LoadedGrid<C,T>> subgrids)
             throws IOException, FactoryException, 
NoninvertibleTransformException
     {
         final TileOrganizer mosaic = new TileOrganizer(null);
@@ -182,7 +181,7 @@ public final class GridGroup<C extends Quantity<C>, T 
extends Quantity<T>> exten
          */
         final Map.Entry<Tile,Tile[]> result = 
CollectionsExt.singletonOrNull(mosaic.tiles().entrySet());
         if (result == null) {
-            throw new 
FactoryException(Resources.format(Resources.Keys.MisalignedDatumShiftGrid_1, 
file));
+            throw new 
FactoryException(Resources.format(Resources.Keys.MisalignedDatumShiftGrid_1, 
file.parameter));
         }
         final Tile global = result.getKey();
         return new GridGroup<>(result.getValue(), grids, 
global.getGridToCRS(), global.getSize());
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridLoader.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridLoader.java
index 17e9e5be3f..0deb5bdc4e 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridLoader.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/GridLoader.java
@@ -16,27 +16,13 @@
  */
 package org.apache.sis.referencing.operation.gridded;
 
-import java.util.logging.Level;
 import java.util.logging.LogRecord;
-import java.util.concurrent.atomic.AtomicBoolean;
 import java.io.EOFException;
 import java.io.IOException;
-import java.io.FileNotFoundException;
 import java.nio.ByteBuffer;
-import java.net.URI;
-import java.nio.file.Path;
-import java.nio.file.Files;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.FileSystemNotFoundException;
 import java.nio.channels.ReadableByteChannel;
-import java.nio.channels.Channels;
-import org.opengis.util.FactoryException;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
-import org.apache.sis.system.DataDirectory;
-import org.apache.sis.referencing.internal.Resources;
-import org.apache.sis.referencing.factory.FactoryDataException;
-import org.apache.sis.referencing.factory.MissingFactoryResourceException;
 import org.apache.sis.referencing.operation.provider.AbstractProvider;
 
 
@@ -72,7 +58,7 @@ public abstract class GridLoader {
     /**
      * The file to load, used for parameter declaration and if we have errors 
to report.
      */
-    protected final URI file;
+    protected final GridFile file;
 
     /**
      * The channel opened on the file.
@@ -84,12 +70,6 @@ public abstract class GridLoader {
      */
     protected final ByteBuffer buffer;
 
-    /**
-     * Whether the tip about the location of datum shift files has been logged.
-     * We log this tip only once, and only if we failed to load at least one 
grid.
-     */
-    private static final AtomicBoolean datumDirectoryLogged = new 
AtomicBoolean();
-
     /**
      * Creates a new loader for the given channel and an existing buffer.
      *
@@ -97,7 +77,7 @@ public abstract class GridLoader {
      * @param  buffer   the buffer to use.
      * @param  file     path to the longitude or latitude difference file. 
Used for parameter declaration and error reporting.
      */
-    protected GridLoader(final ReadableByteChannel channel, final ByteBuffer 
buffer, final URI file) throws IOException {
+    protected GridLoader(final ReadableByteChannel channel, final ByteBuffer 
buffer, final GridFile file) throws IOException {
         this.file    = file;
         this.buffer  = buffer;
         this.channel = channel;
@@ -149,35 +129,6 @@ public abstract class GridLoader {
         buffer.position(p);
     }
 
-    /**
-     * Creates a channel for reading bytes from the file at the specified path.
-     * This method tries to open using the file system before to open from the 
URL.
-     *
-     * @param  path  the path from where to read bytes.
-     * @return a channel for reading bytes from the given path.
-     * @throws IOException if the channel cannot be created.
-     */
-    public static ReadableByteChannel newByteChannel(final URI path) throws 
IOException {
-        try {
-            return Files.newByteChannel(Path.of(path));
-        } catch (FileSystemNotFoundException e) {
-            Logging.ignorableException(AbstractProvider.LOGGER, 
GridLoader.class, "newByteChannel", e);
-        }
-        return Channels.newChannel(path.toURL().openStream());
-    }
-
-    /**
-     * Logs a message about a grid which is about to be loaded.
-     * The logger will be {@code "org.apache.sis.referencing.operation"} and 
the originating
-     * method will be {@code "createMathTransform"} in the specified {@code 
caller} class.
-     *
-     * @param  caller  the provider to logs as the source class.
-     * @param  file    the grid file, as a {@link String} or a {@link URI}.
-     */
-    public static void startLoading(final Class<?> caller, final Object file) {
-        log(caller, Resources.forLocale(null).getLogRecord(Level.FINE, 
Resources.Keys.LoadingDatumShiftFile_1, file));
-    }
-
     /**
      * Logs the given record.
      * The logger will be {@code "org.apache.sis.referencing.operation"} and 
the originating
@@ -189,30 +140,4 @@ public abstract class GridLoader {
     protected static void log(final Class<?> caller, final LogRecord record) {
         Logging.completeAndLog(AbstractProvider.LOGGER, caller, 
"createMathTransform", record);
     }
-
-    /**
-     * Creates the exception to throw when the provider failed to load the 
grid file.
-     *
-     * @param  caller  the provider to logs as the source class if a warning 
occurs.
-     * @param  format  the format name (e.g. "NTv2" or "NADCON").
-     * @param  file    the grid file that the subclass tried to load.
-     * @param  cause   the cause of the failure to load the grid file.
-     */
-    public static FactoryException canNotLoad(final Class<?> caller, final 
String format, final URI file, final Exception cause) {
-        if (!datumDirectoryLogged.get()) {
-            final Path directory = DataDirectory.DATUM_CHANGES.getDirectory();
-            if (directory != null && !datumDirectoryLogged.getAndSet(true)) {
-                log(caller, Resources.forLocale(null).getLogRecord(Level.INFO,
-                            Resources.Keys.DatumChangesDirectory_1, 
directory));
-            }
-        }
-        final boolean notFound = (cause instanceof NoSuchFileException) || 
(cause instanceof FileNotFoundException);
-        final String message = Resources.format(notFound ? 
Resources.Keys.FileNotFound_2
-                                                         : 
Resources.Keys.FileNotReadable_2, format, file);
-        if (notFound) {
-            return new MissingFactoryResourceException(message, cause);
-        } else {
-            return new FactoryDataException(message, cause);
-        }
-    }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/LoadedGrid.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/LoadedGrid.java
index 81c85a28d7..9ba298bb2f 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/LoadedGrid.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/gridded/LoadedGrid.java
@@ -179,7 +179,7 @@ public abstract class LoadedGrid<C extends Quantity<C>, T 
extends Quantity<T>> e
      * @param nx                number of cells along the <var>x</var> axis in 
the grid.
      * @param ny                number of cells along the <var>y</var> axis in 
the grid.
      * @param descriptor        the parameter descriptor of the provider that 
created this grid.
-     * @param files             the file(s) from which the grid has been 
loaded. This array is not cloned.
+     * @param files             the file(s) from which the grid has been 
loaded.
      */
     LoadedGrid(final Unit<C> coordinateUnit,
                final Unit<T> translationUnit,
@@ -188,12 +188,15 @@ public abstract class LoadedGrid<C extends Quantity<C>, T 
extends Quantity<T>> e
                final double Δx, final double Δy,
                final int    nx, final int    ny,
                final ParameterDescriptorGroup descriptor,
-               final URI... files) throws NoninvertibleTransformException
+               final GridFile... sources) throws 
NoninvertibleTransformException
     {
         super(coordinateUnit, new AffineTransform2D(Δx, 0, 0, Δy, x0, 
y0).inverse(),
               new int[] {nx, ny}, isCellValueRatio, translationUnit);
-        this.descriptor     = descriptor;
-        this.files          = files;
+        files = new URI[sources.length];
+        for (int i=0; i<sources.length; i++) {
+            files[i] = sources[i].resolved();
+        }
+        this.descriptor = descriptor;
         this.scanlineStride = nx;
         if (Units.isAngular(coordinateUnit)) {
             periodX = Math.rint((Longitude.MAX_VALUE - Longitude.MIN_VALUE) / 
Math.abs(Δx));
@@ -260,10 +263,13 @@ public abstract class LoadedGrid<C extends Quantity<C>, T 
extends Quantity<T>> e
      *
      * @see GridLoader#canNotLoad(Class, String, URI, Exception)
      */
-    public static LoadedGrid<?,?> getOrLoad(final URI f1, final URI f2, final 
Callable<LoadedGrid<?,?>> loader)
+    public static LoadedGrid<?,?> getOrLoad(final GridFile f1, final GridFile 
f2, final Callable<LoadedGrid<?,?>> loader)
             throws Exception
     {
-        final Object key = (f2 != null) ? new 
AbstractMap.SimpleImmutableEntry<>(f1, f2) : f1;
+        Object key = f1.resolved();
+        if (f2 != null) {
+            key = new AbstractMap.SimpleImmutableEntry<>(key, f2.resolved());
+        }
         return CACHE.getOrCreate(key, loader);
     }
 
@@ -550,7 +556,7 @@ public abstract class LoadedGrid<C extends Quantity<C>, T 
extends Quantity<T>> e
                      final double Δx, final double Δy,
                      final int    nx, final int    ny,
                      final ParameterDescriptorGroup descriptor,
-                     final URI... files) throws NoninvertibleTransformException
+                     final GridFile... files) throws 
NoninvertibleTransformException
         {
             super(coordinateUnit, translationUnit, isCellValueRatio, x0, y0, 
Δx, Δy, nx, ny, descriptor, files);
             offsets = new float[dim][Math.multiplyExact(nx, ny)];
@@ -663,7 +669,7 @@ public abstract class LoadedGrid<C extends Quantity<C>, T 
extends Quantity<T>> e
                       final double Δx, final double Δy,
                       final int    nx, final int    ny,
                       final ParameterDescriptorGroup descriptor,
-                      final URI... files) throws 
NoninvertibleTransformException
+                      final GridFile... files) throws 
NoninvertibleTransformException
         {
             super(coordinateUnit, translationUnit, isCellValueRatio, x0, y0, 
Δx, Δy, nx, ny, descriptor, files);
             offsets = new double[dim][Math.multiplyExact(nx, ny)];
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/FranceGeocentricInterpolation.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/FranceGeocentricInterpolation.java
index c2cbceab50..44585f67ed 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/FranceGeocentricInterpolation.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/FranceGeocentricInterpolation.java
@@ -28,7 +28,6 @@ import java.util.concurrent.Callable;
 import java.io.BufferedReader;
 import java.io.EOFException;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import static java.lang.Float.parseFloat;
 import jakarta.xml.bind.annotation.XmlTransient;
 import javax.measure.quantity.Angle;
@@ -47,6 +46,7 @@ import 
org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.datum.DefaultEllipsoid;
+import org.apache.sis.referencing.operation.gridded.GridFile;
 import org.apache.sis.referencing.operation.gridded.LoadedGrid;
 import org.apache.sis.referencing.operation.gridded.GridLoader;
 import org.apache.sis.referencing.operation.gridded.CompressedGrid;
@@ -59,7 +59,6 @@ import org.apache.sis.measure.Units;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.system.DataDirectory;
 import static org.apache.sis.util.internal.Constants.DIM;
 
 
@@ -268,14 +267,14 @@ public final class FranceGeocentricInterpolation extends 
GeodeticOperation {
     /**
      * Returns {@code true} if the given path seems to be a grid published by 
the French mapping agency for France.
      * In principle this <q>France geocentric interpolation</q> is designed 
specifically for use with the
-     * {@code "gr3df97a.txt"} grid, but in fact the Apache SIS implementation 
should be flexible enough for use
-     * with other area.
+     * {@code "gr3df97a.txt"} grid, but in fact the Apache SIS implementation 
should be flexible enough
+     * for use with other area.
      *
      * @param  file  the grid file.
      * @return {@code true} if the given file looks like a fie from the French 
mapping agency.
      */
-    public static boolean isRecognized(final URI file) {
-        final String filename = file.getPath();
+    public static boolean isRecognized(final GridFile file) {
+        final String filename = file.resolved().getPath();
         final int s = filename.lastIndexOf('/') + 1;
         return filename.regionMatches(true, s, DEFAULT, 0, 5);
     }
@@ -344,13 +343,13 @@ public final class FranceGeocentricInterpolation extends 
GeodeticOperation {
             default: throw new InvalidParameterValueException(Errors.format(
                             Errors.Keys.IllegalArgumentValue_2, DIM, dim), 
DIM, dim);
         }
-        final URI file = pg.getMandatoryValue(FILE);
+        final GridFile file = new GridFile(pg, FILE);
         final LoadedGrid<Angle,Length> grid;
         try {
             grid = getOrLoad(file, isRecognized(file) ? new double[] {TX, TY, 
TZ} : null, PRECISION);
         } catch (Exception e) {
             // NumberFormatException, ArithmeticException, 
NoSuchElementException, and more.
-            throw GridLoader.canNotLoad(FranceGeocentricInterpolation.class, 
HEADER, file, e);
+            throw file.canNotLoad(FranceGeocentricInterpolation.class, HEADER, 
e);
         }
         MathTransform tr = 
InterpolatedGeocentricTransform.createGeodeticTransformation(factory,
                 createEllipsoid(pg, Molodensky.TGT_SEMI_MAJOR,
@@ -377,11 +376,10 @@ public final class FranceGeocentricInterpolation extends 
GeodeticOperation {
      *
      * @see GridLoader#canNotLoad(Class, String, URI, Exception)
      */
-    static LoadedGrid<Angle,Length> getOrLoad(final URI file, final double[] 
averages, final double scale)
+    static LoadedGrid<Angle,Length> getOrLoad(final GridFile file, final 
double[] averages, final double scale)
             throws Exception
     {
-        final URI resolved = DataDirectory.DATUM_CHANGES.toAbsolutePath(file);
-        return LoadedGrid.getOrLoad(resolved, null, new Loader(resolved, 
averages, scale))
+        return LoadedGrid.getOrLoad(file, null, new Loader(file, averages, 
scale))
                                  .castTo(Angle.class, Length.class);
     }
 
@@ -391,7 +389,7 @@ public final class FranceGeocentricInterpolation extends 
GeodeticOperation {
      */
     static final class Loader implements Callable<LoadedGrid<?,?>> {
         /** The file to load. */
-        private final URI file;
+        private final GridFile file;
 
         /** An "average" value for the offset in each dimension, or {@code 
null} if unknown. */
         private final double[] averages;
@@ -400,17 +398,12 @@ public final class FranceGeocentricInterpolation extends 
GeodeticOperation {
         private final double scale;
 
         /** Creates a new loader for the given file. */
-        Loader(final URI file, final double[] averages, final double scale) {
+        Loader(final GridFile file, final double[] averages, final double 
scale) {
             this.file     = file;
             this.averages = averages;
             this.scale    = scale;
         }
 
-        /** Returns the reader for the specified URI. */
-        static BufferedReader newBufferedReader(final URI file) throws 
IOException {
-            return new BufferedReader(new 
InputStreamReader(file.toURL().openStream()));
-        }
-
         /**
          * Invoked when the gridded data are not in the cache.
          * This method load grid data from the file specified at construction 
time.
@@ -423,8 +416,8 @@ public final class FranceGeocentricInterpolation extends 
GeodeticOperation {
         @Override
         public LoadedGrid<?,?> call() throws Exception {
             final LoadedGrid<?,?> grid;
-            try (BufferedReader in = newBufferedReader(file)) {
-                GridLoader.startLoading(FranceGeocentricInterpolation.class, 
file);
+            try (BufferedReader in = file.newBufferedReader()) {
+                file.startLoading(FranceGeocentricInterpolation.class);
                 final LoadedGrid.Float<Angle,Length> g = load(in, file);
                 grid = CompressedGrid.compress(g, averages, scale);
             }
@@ -442,7 +435,7 @@ public final class FranceGeocentricInterpolation extends 
GeodeticOperation {
          * @throws FactoryException if an problem is found with the file 
content.
          * @throws ArithmeticException if the width or the height exceed the 
integer capacity.
          */
-        static LoadedGrid.Float<Angle,Length> load(final BufferedReader in, 
final URI file)
+        static LoadedGrid.Float<Angle,Length> load(final BufferedReader in, 
final GridFile file)
                 throws IOException, FactoryException, 
NoninvertibleTransformException
         {
             LoadedGrid.Float<Angle,Length> grid = null;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NADCON.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NADCON.java
index 4aee4d03f4..43f0709f22 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NADCON.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NADCON.java
@@ -39,12 +39,12 @@ import 
org.apache.sis.referencing.factory.MissingFactoryResourceException;
 import org.apache.sis.referencing.operation.gridded.CompressedGrid;
 import org.apache.sis.referencing.operation.gridded.GridLoader;
 import org.apache.sis.referencing.operation.gridded.LoadedGrid;
+import org.apache.sis.referencing.operation.gridded.GridFile;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.measure.Units;
-import org.apache.sis.system.DataDirectory;
 
 
 /**
@@ -142,9 +142,11 @@ public final class NADCON extends AbstractProvider {
             throws ParameterNotFoundException, FactoryException
     {
         final Parameters pg = Parameters.castOrWrap(values);
+        final GridFile latitudeShifts  = new GridFile(pg, LATITUDE);
+        final GridFile longitudeShifts = new GridFile(pg, LONGITUDE);
         try {
-            return LoadedGrid.createGeodeticTransformation(NADCON.class, 
factory,
-                    getOrLoad(pg.getMandatoryValue(LATITUDE), 
pg.getMandatoryValue(LONGITUDE)));
+            LoadedGrid<Angle,Angle> grid = getOrLoad(latitudeShifts, 
longitudeShifts);
+            return LoadedGrid.createGeodeticTransformation(NADCON.class, 
factory, grid);
         } catch (NoSuchFileException e) {
             throw new MissingFactoryResourceException(e.getMessage(), e);
         } catch (Exception e) {
@@ -160,36 +162,34 @@ public final class NADCON extends AbstractProvider {
      * @param  longitudeShifts  relative or absolute path name of the grid 
file for longitude shifts.
      * @throws Exception if an error occurred while loading the grid.
      */
-    static LoadedGrid<Angle,Angle> getOrLoad(final URI latitudeShifts, final 
URI longitudeShifts)
+    static LoadedGrid<Angle,Angle> getOrLoad(final GridFile latitudeShifts, 
final GridFile longitudeShifts)
             throws Exception
     {
-        final URI rlat = 
DataDirectory.DATUM_CHANGES.toAbsolutePath(latitudeShifts);
-        final URI rlon = 
DataDirectory.DATUM_CHANGES.toAbsolutePath(longitudeShifts);
-        return LoadedGrid.getOrLoad(rlat, rlon, () -> {
+        return LoadedGrid.getOrLoad(latitudeShifts, longitudeShifts, () -> {
             final Loader loader;
-            URI file = latitudeShifts;
+            GridFile file = latitudeShifts;         // Used if an error needs 
to be reported.
             final LoadedGrid<?,?> grid;
             try {
                 // Note: buffer size must be divisible by the size of `float` 
data type.
                 final ByteBuffer buffer = 
ByteBuffer.allocate(4096).order(ByteOrder.LITTLE_ENDIAN);
                 final FloatBuffer fb = buffer.asFloatBuffer();
-                try (ReadableByteChannel in = GridLoader.newByteChannel(rlat)) 
{
-                    GridLoader.startLoading(NADCON.class, 
CharSequences.commonPrefix(
+                try (ReadableByteChannel in = latitudeShifts.newByteChannel()) 
{
+                    GridFile.startLoading(NADCON.class, 
CharSequences.commonPrefix(
                             latitudeShifts.toString(), 
longitudeShifts.toString()).toString() + '…');
-                    loader = new Loader(in, buffer, file);
+                    loader = new Loader(in, buffer, latitudeShifts);
                     loader.readGrid(fb, null, longitudeShifts);
                 }
                 buffer.clear();
                 file = longitudeShifts;
-                try (ReadableByteChannel in = GridLoader.newByteChannel(rlon)) 
{
-                    new Loader(in, buffer, file).readGrid(fb, loader, null);
+                try (ReadableByteChannel in = 
longitudeShifts.newByteChannel()) {
+                    new Loader(in, buffer, longitudeShifts).readGrid(fb, 
loader, null);
                 }
             } catch (IOException | NoninvertibleTransformException | 
RuntimeException e) {
                 /*
                  * Handle the exception here instead of by the caller
                  * because we know which of the 2 files is problematic.
                  */
-                throw GridLoader.canNotLoad(NADCON.class, "NADCON", file, e);
+                throw file.canNotLoad(NADCON.class, "NADCON", e);
             }
             grid = CompressedGrid.compress(loader.grid, null, 
loader.grid.accuracy);
             return grid.useSharedData();
@@ -270,7 +270,7 @@ public final class NADCON extends AbstractProvider {
          *                 and have a capacity divisible by the size of the 
{@code float} type.
          * @param file     path to the longitude or latitude difference file. 
Used for parameter declaration and error reporting.
          */
-        Loader(final ReadableByteChannel channel, final ByteBuffer buffer, 
final URI file)
+        Loader(final ReadableByteChannel channel, final ByteBuffer buffer, 
final GridFile file)
                 throws IOException, FactoryException
         {
             super(channel, buffer, file);
@@ -385,7 +385,7 @@ public final class NADCON extends AbstractProvider {
          * @param longitudeShifts  the file for the longitude grid.
          */
         @SuppressWarnings("lossy-conversions")      // Implicit cast from 
double to float in compound assignment.
-        final void readGrid(final FloatBuffer fb, final Loader latitudeShifts, 
final URI longitudeShifts)
+        final void readGrid(final FloatBuffer fb, final Loader latitudeShifts, 
final GridFile longitudeShifts)
                 throws IOException, FactoryException, 
NoninvertibleTransformException
         {
             final int dim;
@@ -401,7 +401,7 @@ public final class NADCON extends AbstractProvider {
                     y0 != latitudeShifts.y0 || Δy != latitudeShifts.Δy || ny 
!= latitudeShifts.ny || nz != latitudeShifts.nz)
                 {
                     throw new 
FactoryException(Errors.format(Errors.Keys.MismatchedGridGeometry_2,
-                            latitudeShifts.file.getPath(), file.getPath()));
+                            latitudeShifts.file.parameter.getPath(), 
file.parameter.getPath()));
                 }
                 dim   = 0;                                              // 
Dimension of longitudes
                 scale = -DEGREES_TO_SECONDS * Δx;                       // 
NADCON shifts are positive west.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NTv2.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NTv2.java
index 863f795653..1d6218a3a0 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NTv2.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/provider/NTv2.java
@@ -46,6 +46,7 @@ import 
org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.gridded.CompressedGrid;
 import org.apache.sis.referencing.operation.gridded.GridLoader;
 import org.apache.sis.referencing.operation.gridded.GridGroup;
+import org.apache.sis.referencing.operation.gridded.GridFile;
 import org.apache.sis.referencing.operation.gridded.LoadedGrid;
 import org.apache.sis.referencing.util.Formulas;
 import org.apache.sis.referencing.internal.Resources;
@@ -56,7 +57,6 @@ import org.apache.sis.util.internal.Strings;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Messages;
 import org.apache.sis.measure.Units;
-import org.apache.sis.system.DataDirectory;
 
 
 /**
@@ -145,13 +145,12 @@ public final class NTv2 extends AbstractProvider {
             final MathTransformFactory factory, final ParameterValueGroup 
values, final int version)
             throws ParameterNotFoundException, FactoryException
     {
-        final Parameters pg = Parameters.castOrWrap(values);
-        final URI file = pg.getMandatoryValue(FILE);
+        final GridFile file = new GridFile(Parameters.castOrWrap(values), 
FILE);
         final LoadedGrid<Angle,Angle> grid;
         try {
             grid = getOrLoad(provider, file, version);
         } catch (Exception e) {
-            throw GridLoader.canNotLoad(provider, provider.getSimpleName(), 
file, e);
+            throw file.canNotLoad(provider, provider.getSimpleName(), e);
         }
         return LoadedGrid.createGeodeticTransformation(provider, factory, 
grid);
     }
@@ -169,13 +168,12 @@ public final class NTv2 extends AbstractProvider {
      * @see GridLoader#canNotLoad(String, URI, Exception)
      */
     static LoadedGrid<Angle,Angle> getOrLoad(final Class<? extends 
AbstractProvider> provider,
-            final URI file, final int version) throws Exception
+            final GridFile file, final int version) throws Exception
     {
-        final URI resolved = DataDirectory.DATUM_CHANGES.toAbsolutePath(file);
-        return LoadedGrid.getOrLoad(resolved, null, () -> {
+        return LoadedGrid.getOrLoad(file, null, () -> {
             final LoadedGrid<?,?> grid;
-            try (ReadableByteChannel in = GridLoader.newByteChannel(resolved)) 
{
-                GridLoader.startLoading(provider, file);
+            try (ReadableByteChannel in = file.newByteChannel()) {
+                file.startLoading(provider);
                 final Loader loader = new Loader(in, file, version);
                 grid = loader.readAllGrids();
                 loader.report(provider);
@@ -318,7 +316,7 @@ public final class NTv2 extends AbstractProvider {
          * @param  version  the expected version (1 or 2).
          * @throws FactoryException if a data record cannot be parsed.
          */
-        Loader(final ReadableByteChannel channel, final URI file, int version) 
throws IOException, FactoryException {
+        Loader(final ReadableByteChannel channel, final GridFile file, int 
version) throws IOException, FactoryException {
             super(channel, ByteBuffer.allocate(4096), file);
             header = new LinkedHashMap<>();
             ensureBufferContains(RECORD_LENGTH);
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/DatumShiftTestCase.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/DatumShiftTestCase.java
index 915c1bebc2..7654b5c8e7 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/DatumShiftTestCase.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/DatumShiftTestCase.java
@@ -19,6 +19,7 @@ package org.apache.sis.referencing.operation.provider;
 import java.net.URI;
 import java.net.URL;
 import java.net.URISyntaxException;
+import org.apache.sis.referencing.operation.gridded.GridFile;
 
 // Test dependencies
 import static org.junit.Assert.*;
@@ -64,11 +65,11 @@ public abstract class DatumShiftTestCase extends TestCase {
      * @param  name  name of the resource to get.
      * @return the requested resources.
      */
-    static URI getResource(final String name) throws URISyntaxException {
+    static GridFile getResource(final String name) throws URISyntaxException {
         final URL file = getResourceAsConvertibleURL(name);
         if (file == null) {
             assumeFalse("Cannot read grid data in a JAR file.", 
"jar".equals(file.getProtocol()));
         }
-        return file.toURI();
+        return new GridFile(file.toURI());
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/FranceGeocentricInterpolationTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/FranceGeocentricInterpolationTest.java
index d24a15cac0..cf741ae2f2 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/FranceGeocentricInterpolationTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/FranceGeocentricInterpolationTest.java
@@ -25,6 +25,7 @@ import javax.measure.quantity.Length;
 import org.opengis.geometry.Envelope;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.gridded.GridFile;
 import org.apache.sis.referencing.operation.gridded.LoadedGrid;
 import org.apache.sis.referencing.operation.gridded.CompressedGrid;
 
@@ -101,10 +102,10 @@ public final class FranceGeocentricInterpolationTest 
extends DatumShiftTestCase
      */
     @Test
     public void testIsRecognized() throws URISyntaxException {
-        assertTrue (FranceGeocentricInterpolation.isRecognized(new 
URI("GR3DF97A.txt")));
-        assertTrue (FranceGeocentricInterpolation.isRecognized(new 
URI("gr3df")));
-        assertFalse(FranceGeocentricInterpolation.isRecognized(new 
URI("gr3d")));
-        assertTrue (FranceGeocentricInterpolation.isRecognized(new 
URI(TEST_FILE)));
+        assertTrue (FranceGeocentricInterpolation.isRecognized(new 
GridFile(new URI("GR3DF97A.txt"))));
+        assertTrue (FranceGeocentricInterpolation.isRecognized(new 
GridFile(new URI("gr3df"))));
+        assertFalse(FranceGeocentricInterpolation.isRecognized(new 
GridFile(new URI("gr3d"))));
+        assertTrue (FranceGeocentricInterpolation.isRecognized(new 
GridFile(new URI(TEST_FILE))));
     }
 
     /**
@@ -144,9 +145,9 @@ public final class FranceGeocentricInterpolationTest 
extends DatumShiftTestCase
     private static LoadedGrid<Angle,Length> testGridAsFloats()
             throws URISyntaxException, IOException, FactoryException, 
TransformException
     {
-        final URI file = getResource(TEST_FILE);
+        final GridFile file = getResource(TEST_FILE);
         final LoadedGrid.Float<Angle,Length> grid;
-        try (BufferedReader in = 
FranceGeocentricInterpolation.Loader.newBufferedReader(file)) {
+        try (BufferedReader in = file.newBufferedReader()) {
             grid = FranceGeocentricInterpolation.Loader.load(in, file);
         }
         assertEquals("cellPrecision",   0.005, grid.getCellPrecision(), 
STRICT);
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/NADCONTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/NADCONTest.java
index 374aa14b37..e6179f07ee 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/NADCONTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/NADCONTest.java
@@ -25,8 +25,9 @@ import java.nio.file.Files;
 import javax.measure.quantity.Angle;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.referencing.operation.gridded.LoadedGrid;
+import org.apache.sis.referencing.operation.gridded.GridFile;
 import org.apache.sis.referencing.operation.gridded.GridLoader;
+import org.apache.sis.referencing.operation.gridded.LoadedGrid;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Envelopes;
@@ -120,7 +121,7 @@ public final class NADCONTest extends DatumShiftTestCase {
      * @param  longitudeShifts  path to the official {@code "conus.los"} file.
      * @throws Exception if an error occurred while loading or computing the 
grid, or while testing transformations.
      */
-    public static void testNADCON(final URI latitudeShifts, final URI 
longitudeShifts) throws Exception {
+    public static void testNADCON(final GridFile latitudeShifts, final 
GridFile longitudeShifts) throws Exception {
         testNADCON(latitudeShifts, longitudeShifts, -131, -63, 20, 50);
     }
 
@@ -132,7 +133,7 @@ public final class NADCONTest extends DatumShiftTestCase {
      * @param  ymin  southmost latitude.
      * @param  ymax  northmost latitude.
      */
-    private static void testNADCON(final URI latitudeShifts, final URI 
longitudeShifts,
+    private static void testNADCON(final GridFile latitudeShifts, final 
GridFile longitudeShifts,
             final double xmin, final double xmax, final double ymin, final 
double ymax)
             throws Exception
     {
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/NTv2Test.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/NTv2Test.java
index b28c08f035..0d803d1d5c 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/NTv2Test.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/provider/NTv2Test.java
@@ -28,14 +28,16 @@ import java.nio.charset.StandardCharsets;
 import javax.measure.quantity.Angle;
 import org.opengis.geometry.Envelope;
 import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.referencing.operation.gridded.LoadedGrid;
+import org.apache.sis.referencing.util.Formulas;
+import org.apache.sis.referencing.operation.matrix.Matrix3;
+import org.apache.sis.referencing.operation.gridded.GridFile;
 import org.apache.sis.referencing.operation.gridded.GridGroup;
+import org.apache.sis.referencing.operation.gridded.LoadedGrid;
 import static 
org.apache.sis.referencing.operation.gridded.GridLoader.DEGREES_TO_SECONDS;
-import org.apache.sis.referencing.operation.matrix.Matrix3;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.measure.Units;
-import org.apache.sis.referencing.util.Formulas;
+import org.apache.sis.parameter.Parameters;
 import org.apache.sis.system.DataDirectory;
 
 // Test dependencies
@@ -109,7 +111,7 @@ public final class NTv2Test extends DatumShiftTestCase {
      * @param  file  path to the official {@code "NTF_R93.gsb"} file.
      * @throws Exception if an error occurred while loading or computing the 
grid, or while testing transformations.
      */
-    public static void testRGF93(final URI file) throws Exception {
+    public static void testRGF93(final GridFile file) throws Exception {
         testRGF93(file, -19800, 36000, 147600, 187200);
     }
 
@@ -121,7 +123,7 @@ public final class NTv2Test extends DatumShiftTestCase {
      * @param  ymin  value of the {@code "S_LAT"} record.
      * @param  ymax  value of the {@code "N_LAT"} record.
      */
-    private static void testRGF93(final URI file, final double xmin, final 
double xmax,
+    private static void testRGF93(final GridFile file, final double xmin, 
final double xmax,
             final double ymin, final double ymax) throws Exception
     {
         final double cellSize = 360;
@@ -188,8 +190,11 @@ public final class NTv2Test extends DatumShiftTestCase {
     public void testMultiGrids() throws Exception {
         assumeTrue(RUN_EXTENSIVE_TESTS);
         assumeTrue(DataDirectory.getenv() != null);
-        final URI file = DataDirectory.DATUM_CHANGES.toAbsolutePath(new 
URI(MULTIGRID_TEST_FILE));
-        assumeTrue(Files.exists(Path.of(file)));
+        final Parameters pg = Parameters.castOrWrap(new 
NTv2().getParameters().createValue());
+        pg.getOrCreate(NTv2.FILE).setValue(new URI(MULTIGRID_TEST_FILE));
+        final GridFile file = new GridFile(pg, NTv2.FILE);
+        assumeTrue(Files.exists(Path.of(file.resolved())));
+
         final LoadedGrid<Angle,Angle> grid = NTv2.getOrLoad(NTv2.class, file, 
2);
         assertInstanceOf("Should contain many grids.", GridGroup.class, grid);
         assertEquals("coordinateUnit",  Units.ARC_SECOND, 
grid.getCoordinateUnit());
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/DatumShiftTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/DatumShiftTest.java
index e5d186752b..1fc24344a2 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/DatumShiftTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/test/integration/DatumShiftTest.java
@@ -18,6 +18,7 @@ package org.apache.sis.test.integration;
 
 import java.net.URI;
 import org.apache.sis.system.DataDirectory;
+import org.apache.sis.referencing.operation.gridded.GridFile;
 
 // Test dependencies
 import org.junit.Test;
@@ -57,7 +58,7 @@ public final class DatumShiftTest extends TestCase {
     @Test
     public void testRGF93() throws Exception {
         final URI file = assumeDataExists(DataDirectory.DATUM_CHANGES, 
"ntf_r93.gsb");
-        NTv2Test.testRGF93(file);
+        NTv2Test.testRGF93(new GridFile(file));
     }
 
     /**
@@ -70,6 +71,6 @@ public final class DatumShiftTest extends TestCase {
     public void testNADCON() throws Exception {
         final URI latitudeShifts  = 
assumeDataExists(DataDirectory.DATUM_CHANGES, "conus.las");
         final URI longitudeShifts = 
assumeDataExists(DataDirectory.DATUM_CHANGES, "conus.los");
-        NADCONTest.testNADCON(latitudeShifts, longitudeShifts);
+        NADCONTest.testNADCON(new GridFile(latitudeShifts), new 
GridFile(longitudeShifts));
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/DataDirectory.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/DataDirectory.java
index 81daa118a4..025851083c 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/DataDirectory.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/DataDirectory.java
@@ -21,12 +21,10 @@ import java.net.URI;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.InvalidPathException;
-import java.nio.file.NoSuchFileException;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.internal.Strings;
-import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Messages;
 
 
@@ -238,7 +236,7 @@ public enum DataDirectory {
      *
      * @return the sub-directory, or {@code null} if unspecified.
      */
-    private synchronized URI getDirectoryAsURI() {
+    public final synchronized URI getDirectoryAsURI() {
         if (directoryAsURI == null) {
             @SuppressWarnings("LocalVariableHidesMemberVariable")
             final Path directory = getDirectory();
@@ -252,36 +250,4 @@ public enum DataDirectory {
         }
         return directoryAsURI;
     }
-
-    /**
-     * Returns the given URI as an absolute URI, resolved with this {@code 
DataDirectory} if the URI is relative.
-     * If the URI cannot be made absolute, a {@link NoSuchFileException} is 
thrown. This is necessary for letting
-     * the caller know that a coordinate operation is probably valid but 
cannot be constructed because an optional
-     * configuration is missing. It is typically because the {@code SIS_DATA} 
environment variable has not been set.
-     *
-     * @param  path  the URI to make absolute.
-     * @return an absolute URI to the data.
-     * @throws NoSuchFileException if the path cannot be made absolute.
-     */
-    public final URI toAbsolutePath(URI path) throws NoSuchFileException {
-        final URI base = getDirectoryAsURI();
-        if (base != null) {
-            path = base.resolve(path);
-        }
-        if (path.isAbsolute()) {
-            return path;
-        }
-        final String message;
-        if (path.isOpaque()) {
-            message = Errors.format(Errors.Keys.CanNotOpen_1, path);
-        } else {
-            final String env = getenv();
-            if (env == null) {
-                message = 
Messages.format(Messages.Keys.DataDirectoryNotSpecified_1, ENV);
-            } else {
-                message = 
Messages.format(Messages.Keys.DataDirectoryNotAccessible_2, ENV, env);
-            }
-        }
-        throw new NoSuchFileException(path.toString(), null, message);
-    }
 }

Reply via email to