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

[sis] 03/03: Replace `Path` parameter value by `URI` in operation methods using datum shift grids.

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

commit 8991c55d92826eb9fcda9f3d0570adfedbc33a77
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Jan 14 17:31:56 2023 +0100

    Replace `Path` parameter value by `URI` in operation methods using datum shift grids.
    
    https://issues.apache.org/jira/browse/SIS-569
---
 .../main/java/org/apache/sis/gui/RecentFiles.java  |   2 +-
 .../referencing/provider/DatumShiftGridFile.java   |  65 ++--
 .../referencing/provider/DatumShiftGridGroup.java  |   6 +-
 .../referencing/provider/DatumShiftGridLoader.java |  53 ++-
 .../provider/FranceGeocentricInterpolation.java    | 359 +++++++++++----------
 .../provider/MolodenskyInterpolation.java          |   1 +
 .../sis/internal/referencing/provider/NADCON.java  |  93 +++---
 .../sis/internal/referencing/provider/NTv2.java    |  54 ++--
 .../referencing/provider/DatumShiftTestCase.java   |  23 +-
 .../FranceGeocentricInterpolationTest.java         |  34 +-
 .../internal/referencing/provider/NADCONTest.java  |  11 +-
 .../internal/referencing/provider/NTv2Test.java    |  19 +-
 .../sis/test/integration/DatumShiftTest.java       |  10 +-
 .../apache/sis/internal/system/DataDirectory.java  |  23 +-
 .../src/test/java/org/apache/sis/test/Assume.java  |   7 +-
 .../java/org/apache/sis/test/package-info.java     |   2 +-
 16 files changed, 403 insertions(+), 359 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/RecentFiles.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/RecentFiles.java
index 5ea3439497..7e20c151c2 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/RecentFiles.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/RecentFiles.java
@@ -121,7 +121,7 @@ final class RecentFiles implements EventHandler<ActionEvent> {
         try {
             file = path.toFile();
         } catch (UnsupportedOperationException e) {
-            // Future version may have an "recently used URI" section. We don't do that for now.
+            // Recently used URIs are not saved here.
             return;
         }
         final int size = items.size();
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java
index 70510c524d..5b5cf52834 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridFile.java
@@ -18,12 +18,14 @@ package org.apache.sis.internal.referencing.provider;
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.AbstractMap;
 import java.util.LinkedHashMap;
 import java.util.Locale;
 import java.util.Map;
 import java.util.logging.Level;
+import java.util.concurrent.Callable;
 import java.lang.reflect.Array;
-import java.nio.file.Path;
+import java.net.URI;
 import javax.measure.Unit;
 import javax.measure.Quantity;
 import javax.measure.quantity.Angle;
@@ -47,10 +49,11 @@ import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.referencing.datum.DatumShiftGrid;
-import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
+import org.apache.sis.referencing.factory.FactoryDataException;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.InterpolatedTransform;
+import org.apache.sis.internal.referencing.j2d.AffineTransform2D;
 
 
 /**
@@ -62,7 +65,7 @@ import org.apache.sis.referencing.operation.transform.InterpolatedTransform;
  * sharing data and for {@link #equals(Object)} and {@link #hashCode()} implementations.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  *
  * @param <C>  dimension of the coordinate unit (usually {@link Angle}).
  * @param <T>  dimension of the translation unit. Usually {@link Angle},
@@ -76,10 +79,10 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = -5801692909082130314L;
+    private static final long serialVersionUID = -1690433946781367085L;
 
     /**
-     * Cache of grids loaded so far. The keys are typically {@link java.nio.file.Path}s or a tuple of paths.
+     * Cache of grids loaded so far. The keys are typically {@link URI}s or a tuple of URIs.
      * Values are grids stored by hard references until the amount of data exceed 32768 (about 128 kilobytes
      * if the values use the {@code float} type), in which case the oldest grids will be replaced by soft references.
      *
@@ -87,8 +90,10 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
      * The use of soft references instead of weak references is on the assumption that users typically use
      * the same few Coordinate Reference Systems for their work. Consequently, we presume that users will not
      * load a lot of grids and are likely to reuse the already loaded grids.
+     *
+     * @see #getOrLoad(URI, URI, Callable)
      */
-    static final Cache<Object, DatumShiftGridFile<?,?>> CACHE = new Cache<Object, DatumShiftGridFile<?,?>>(4, 32*1024, true) {
+    private static final Cache<Object, DatumShiftGridFile<?,?>> CACHE = new Cache<Object, DatumShiftGridFile<?,?>>(4, 32*1024, true) {
         @Override protected int cost(final DatumShiftGridFile<?,?> grid) {
             int p = 1;
             for (final Object data : grid.getData()) {
@@ -112,10 +117,8 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
      * The files from which the grid has been loaded. This is not used directly by this class
      * (except for {@link #equals(Object)} and {@link #hashCode()}), but can be used by math
      * transform for setting the parameter values. Shall never be null and never empty.
-     *
-     * @todo We have a serialization problem here. Possible workaround may be to replace by URI.
      */
-    private final Path[] files;
+    private final URI[] files;
 
     /**
      * Number of cells between the start of adjacent rows in the grid. This is usually {@code getGridSize(0)},
@@ -150,14 +153,14 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
      * in the domain of validity of this grid. Children do not change the way this {@code DatumShiftGrid}
      * performs its calculation; this list is used only at the time of building {@link MathTransform} tree.
      *
-     * <div class="note"><b>Design note:</b>
+     * <h4>Design note</h4>
      * we do not provide sub-grids functionality in the {@link DatumShiftGrid} parent class because
      * the {@link MathTransform} tree will depend on assumptions about {@link #getCoordinateToGrid()},
      * in particular that it contains only translations and scales (no rotation, no shear).
-     * Those assumptions are enforced by the {@link DatumShiftGridFile} constructor.</div>
+     * Those assumptions are enforced by the {@link DatumShiftGridFile} constructor.
      *
-     * This field has protected access for usage by {@link DatumShiftGridGroup} subclass only.
-     * No access to this field should be done except by subclasses.
+     * <p>This field has protected access for usage by {@link DatumShiftGridGroup} subclass only.
+     * No access to this field should be done except by subclasses.</p>
      *
      * @see #setSubGrids(Collection)
      */
@@ -186,7 +189,7 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
                        final double Δx, final double Δy,
                        final int    nx, final int    ny,
                        final ParameterDescriptorGroup descriptor,
-                       final Path... files) throws NoninvertibleTransformException
+                       final URI... files) throws NoninvertibleTransformException
     {
         super(coordinateUnit, new AffineTransform2D(Δx, 0, 0, Δy, x0, y0).inverse(),
               new int[] {nx, ny}, isCellValueRatio, translationUnit);
@@ -246,6 +249,28 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
         // Accuracy to be set by caller. Initial value needs to be zero.
     }
 
+    /**
+     * Gets the grid from the cache if available, or loads it.
+     *
+     * @param  f1      the main file to load.
+     * @param  f2      a second file to load, or {@code null} if none.
+     * @param  loader  the loader to execute if the grid is not in the cache.
+     * @return the cached or loaded grid.
+     * @throws FactoryException if an error occurred while loading the grid.
+     */
+    static DatumShiftGridFile<?,?> getOrLoad(final URI f1, final URI f2, final Callable<DatumShiftGridFile<?,?>> loader)
+            throws FactoryException
+    {
+        final Object key = (f2 != null) ? new AbstractMap.SimpleImmutableEntry<>(f1, f2) : f1;
+        try {
+            return CACHE.getOrCreate(key, loader);
+        } catch (FactoryException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new FactoryDataException(Errors.format(Errors.Keys.CanNotRead_1, f1), e);
+        }
+    }
+
     /**
      * Sets the sub-grids that are direct children of this grid.
      * This method can be invoked only once.
@@ -288,6 +313,7 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
 
     /**
      * Formats this grid as a tree with its children.
+     * Used for building a tree representation of children nodes.
      */
     private void toTree(final TreeTable.Node branch) {
         String label = super.toString();
@@ -420,8 +446,7 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
     }
 
     /**
-     * Sets all parameters for a value of type {@link Path} to the values given to the constructor.
-     * Subclasses may override for defining other kinds of parameters too.
+     * Sets all parameters for a value of type {@link URI} to the values given to the constructor.
      *
      * @param  parameters  the parameter group where to set the values.
      */
@@ -431,8 +456,8 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
         for (final GeneralParameterDescriptor gd : descriptor.descriptors()) {
             if (gd instanceof ParameterDescriptor<?>) {
                 final ParameterDescriptor<?> d = (ParameterDescriptor<?>) gd;
-                if (Path.class.isAssignableFrom(d.getValueClass())) {
-                    if (i >= files.length) break;                               // Safety in case of invalid parameters.
+                if (URI.class.isAssignableFrom(d.getValueClass())) {
+                    if (i >= files.length) break;                       // Safety in case of invalid parameters.
                     parameters.getOrCreate(d).setValue(files[i++]);
                 }
             }
@@ -519,7 +544,7 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
               final double Δx, final double Δy,
               final int    nx, final int    ny,
               final ParameterDescriptorGroup descriptor,
-              final Path... files) throws NoninvertibleTransformException
+              final URI... files) throws NoninvertibleTransformException
         {
             super(coordinateUnit, translationUnit, isCellValueRatio, x0, y0, Δx, Δy, nx, ny, descriptor, files);
             offsets = new float[dim][Math.multiplyExact(nx, ny)];
@@ -631,7 +656,7 @@ abstract class DatumShiftGridFile<C extends Quantity<C>, T extends Quantity<T>>
                final double Δx, final double Δy,
                final int    nx, final int    ny,
                final ParameterDescriptorGroup descriptor,
-               final Path... files) throws NoninvertibleTransformException
+               final URI... 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/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridGroup.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridGroup.java
index 7d98c5b849..7e86ebb069 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridGroup.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridGroup.java
@@ -18,12 +18,12 @@ package org.apache.sis.internal.referencing.provider;
 
 import java.util.Map;
 import java.util.List;
-import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.net.URI;
 import java.io.IOException;
 import java.awt.Dimension;
 import java.awt.Rectangle;
 import java.awt.geom.AffineTransform;
-import java.util.LinkedHashMap;
 import javax.measure.Quantity;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
@@ -155,7 +155,7 @@ final class DatumShiftGridGroup<C extends Quantity<C>, T extends Quantity<T>> ex
      * @throws IOException declared because {@link Tile#getRegion()} declares it, but should not happen.
      */
     static <C extends Quantity<C>, T extends Quantity<T>> DatumShiftGridGroup<C,T> create(
-            final Path file, final List<DatumShiftGridFile<C,T>> subgrids)
+            final URI file, final List<DatumShiftGridFile<C,T>> subgrids)
             throws IOException, FactoryException, NoninvertibleTransformException
     {
         final TileOrganizer mosaic = new TileOrganizer(null);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
index fdc1a11cc7..1db194ec33 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/DatumShiftGridLoader.java
@@ -17,18 +17,25 @@
 package org.apache.sis.internal.referencing.provider;
 
 import java.util.logging.Level;
+import java.util.logging.Logger;
 import java.util.logging.LogRecord;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.io.EOFException;
 import java.io.IOException;
+import java.net.URI;
 import java.nio.ByteBuffer;
 import java.nio.file.Path;
+import java.nio.file.Paths;
+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.internal.system.Loggers;
+import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.referencing.factory.FactoryDataException;
@@ -37,12 +44,13 @@ import org.apache.sis.referencing.factory.MissingFactoryResourceException;
 
 /**
  * Base class of datum shift grid loaders.
+ * This loader uses {@link ReadableByteChannel}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  * @since   0.7
  */
-class DatumShiftGridLoader {
+abstract class DatumShiftGridLoader {
     /**
      * Conversion factor from degrees to seconds.
      */
@@ -68,7 +76,7 @@ class DatumShiftGridLoader {
     /**
      * The file to load, used for parameter declaration and if we have errors to report.
      */
-    final Path file;
+    final URI file;
 
     /**
      * The channel opened on the file.
@@ -93,7 +101,7 @@ class DatumShiftGridLoader {
      * @param  buffer   the buffer to use.
      * @param  file     path to the longitude or latitude difference file. Used for parameter declaration and error reporting.
      */
-    DatumShiftGridLoader(final ReadableByteChannel channel, final ByteBuffer buffer, final Path file) throws IOException {
+    DatumShiftGridLoader(final ReadableByteChannel channel, final ByteBuffer buffer, final URI file) throws IOException {
         this.file    = file;
         this.buffer  = buffer;
         this.channel = channel;
@@ -143,12 +151,45 @@ class DatumShiftGridLoader {
         buffer.position(p);
     }
 
+    /**
+     * If the given URI is not absolute, tries to make it absolute
+     * with a path to the common directory of datum shift grid files.
+     *
+     * @param  path  the URI to make absolute.
+     * @return an absolute (if possible) URI to the data.
+     */
+    static URI toAbsolutePath(final URI path) {
+        if (!path.isAbsolute() && !path.isOpaque()) {
+            final Path dir = DataDirectory.DATUM_CHANGES.getDirectory();
+            if (dir != null) {
+                return dir.resolve(path.getPath()).toUri();
+            }
+        }
+        return path;
+    }
+
+    /**
+     * Creates a channel for reading bytes from the file at the specified path.
+     *
+     * @param  path  the path from where to read bytes.
+     * @return a channel for reading bytes from the given path.
+     * @throws IOException if the channel can not be created.
+     */
+    static ReadableByteChannel newByteChannel(final URI path) throws IOException {
+        try {
+            return Files.newByteChannel(Paths.get(path));
+        } catch (FileSystemNotFoundException e) {
+            Logging.ignorableException(Logger.getLogger(Modules.REFERENCING), DatumShiftGridLoader.class, "newByteChannel", e);
+        }
+        return Channels.newChannel(path.toURL().openStream());
+    }
+
     /**
      * Logs a message about a grid which is about to be loaded.
      *
      * @param  caller  the provider to logs as the source class.
      *                 the source method will be set to {@code "createMathTransform"}.
-     * @param  file    the grid file, as a {@link String} or a {@link Path}.
+     * @param  file    the grid file, as a {@link String} or a {@link URI}.
      */
     static void startLoading(final Class<?> caller, final Object file) {
         log(caller, Resources.forLocale(null).getLogRecord(Level.FINE, Resources.Keys.LoadingDatumShiftFile_1, file));
@@ -173,7 +214,7 @@ class DatumShiftGridLoader {
      * @param  file    the grid file that the subclass tried to load.
      * @param  cause   the cause of the failure to load the grid file.
      */
-    static FactoryException canNotLoad(final String format, final Path file, final Exception cause) {
+    static FactoryException canNotLoad(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)) {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
index ddb61882f5..2e7b64259e 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolation.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.internal.referencing.provider;
 
+import java.net.URI;
 import java.util.Map;
 import java.util.Arrays;
 import java.util.Locale;
@@ -23,12 +24,11 @@ import java.util.NoSuchElementException;
 import java.util.StringTokenizer;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import java.util.concurrent.Callable;
 import java.io.BufferedReader;
 import java.io.EOFException;
 import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.Files;
+import java.io.InputStreamReader;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.measure.quantity.Angle;
 import javax.measure.quantity.Length;
@@ -45,7 +45,6 @@ import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.opengis.util.FactoryException;
 import org.apache.sis.internal.system.Loggers;
-import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.internal.referencing.NilReferencingObject;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.parameter.ParameterBuilder;
@@ -54,7 +53,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.util.collection.Cache;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.datum.DefaultEllipsoid;
 import org.apache.sis.referencing.operation.transform.InterpolatedGeocentricTransform;
@@ -87,7 +85,7 @@ import static org.apache.sis.internal.util.Constants.DIM;
  *
  * @author  Simon Reynard (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   0.7
  */
 @XmlTransient
@@ -95,7 +93,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
     /**
      * Serial number for inter-operability with different versions.
      */
-    private static final long serialVersionUID = -4707304160205218546L;
+    private static final long serialVersionUID = -298193260915837911L;
 
     /**
      * Geocentric translation parameters to use as a first guess before to use the grid in France.
@@ -137,7 +135,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
      * Name of the default grid file, as mentioned in the NTG_88 document.
      * We use the 5 first characters ({@code "gr3df"}) as a sentinel value for French grid file.
      *
-     * @see #isRecognized(Path)
+     * @see #isRecognized(URI)
      */
     private static final String DEFAULT = "gr3df97a.txt";
 
@@ -154,7 +152,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
      *   <li>Default value: {@code gr3df97a.txt}</li>
      * </ul>
      */
-    public static final ParameterDescriptor<Path> FILE;
+    public static final ParameterDescriptor<URI> FILE;
 
     /**
      * The operation parameter descriptor for the <cite>EPSG code for Interpolation CRS</cite> parameter value.
@@ -197,7 +195,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
         FILE = builder
                 .addIdentifier("8727")
                 .addName("Geocentric translation file")
-                .create(Path.class, Paths.get(DEFAULT));
+                .create(URI.class, URI.create(DEFAULT));
         INTERPOLATION_CRS = builder
                 .addIdentifier("1048")
                 .addName("EPSG code for Interpolation CRS")
@@ -258,8 +256,10 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
      * @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 Path file) {
-        return file.getFileName().toString().regionMatches(true, 0, DEFAULT, 0, 5);
+    public static boolean isRecognized(final URI file) {
+        final String filename = file.getPath();
+        final int s = filename.lastIndexOf('/') + 1;
+        return filename.regionMatches(true, s, DEFAULT, 0, 5);
     }
 
     /**
@@ -326,7 +326,7 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
             default: throw new InvalidParameterValueException(Errors.format(
                             Errors.Keys.IllegalArgumentValue_2, DIM, dim), DIM, dim);
         }
-        final Path file = pg.getMandatoryValue(FILE);
+        final URI file = pg.getMandatoryValue(FILE);
         final DatumShiftGridFile<Angle,Length> grid = getOrLoad(file,
                 isRecognized(file) ? new double[] {TX, TY, TZ} : null, PRECISION);
 
@@ -361,176 +361,205 @@ public class FranceGeocentricInterpolation extends GeodeticOperation {
      * Returns the grid of the given name. This method returns the cached instance if it still exists,
      * or load the grid otherwise.
      *
-     * @param  file      name of the datum shift grid file to load.
+     * @param  file      an absolute or relative reference to the datum shift grid file to load.
      * @param  averages  an "average" value for the offset in each dimension, or {@code null} if unknown.
      * @param  scale     the factor by which to multiply each compressed value before to add to the average value.
      */
-    static DatumShiftGridFile<Angle,Length> getOrLoad(final Path file, final double[] averages, final double scale)
+    static DatumShiftGridFile<Angle,Length> getOrLoad(final URI file, final double[] averages, final double scale)
             throws FactoryException
     {
-        final Path resolved = DataDirectory.DATUM_CHANGES.resolve(file).toAbsolutePath();
-        DatumShiftGridFile<?,?> grid = DatumShiftGridFile.CACHE.peek(resolved);
-        if (grid == null) {
-            final Cache.Handler<DatumShiftGridFile<?,?>> handler = DatumShiftGridFile.CACHE.lock(resolved);
-            try {
-                grid = handler.peek();
-                if (grid == null) {
-                    try (BufferedReader in = Files.newBufferedReader(resolved)) {
-                        DatumShiftGridLoader.startLoading(FranceGeocentricInterpolation.class, file);
-                        final DatumShiftGridFile.Float<Angle,Length> g = load(in, file);
-                        grid = DatumShiftGridCompressed.compress(g, averages, scale);
-                    } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
-                        // NumberFormatException, ArithmeticException, NoSuchElementException, possibly other.
-                        throw DatumShiftGridLoader.canNotLoad(HEADER, file, e);
-                    }
-                    grid = grid.useSharedData();
-                }
-            } finally {
-                handler.putAndUnlock(grid);
-            }
-        }
-        return grid.castTo(Angle.class, Length.class);
+        final URI resolved = DatumShiftGridLoader.toAbsolutePath(file);
+        return DatumShiftGridFile.getOrLoad(resolved, null, new Loader(resolved, averages, scale))
+                                 .castTo(Angle.class, Length.class);
     }
 
     /**
-     * Unconditionally loads the grid for the given file without in-memory compression.
-     *
-     * @param  in    reader of the RGF93 datum shift file.
-     * @param  file  path to the file being read, used for parameter declaration and error reporting.
-     * @throws IOException if an I/O error occurred.
-     * @throws NumberFormatException if a number cannot be parsed.
-     * @throws NoSuchElementException if a data line is missing a value.
-     * @throws FactoryException if an problem is found with the file content.
-     * @throws ArithmeticException if the width or the height exceed the integer capacity.
+     * Temporary object created for loading gridded data if not present in the cache.
+     * The data are provided in a text file, which is read with {@link BufferedReader}.
      */
-    static DatumShiftGridFile.Float<Angle,Length> load(final BufferedReader in, final Path file)
-            throws IOException, FactoryException, NoninvertibleTransformException
-    {
-        DatumShiftGridFile.Float<Angle,Length> grid = null;
-        double x0 = 0;
-        double xf = 0;
-        double y0 = 0;
-        double yf = 0;
-        double Δx = 0;
-        double Δy = 0;
-        int    nx = 0;
-        int    ny = 0;
-        /*
-         * The header should be like below, but the only essential line for this class is the one
-         * starting with "GR3D1". We also check that "GR3D2" declares the expected interpolation.
+    static final class Loader implements Callable<DatumShiftGridFile<?,?>> {
+        /** The file to load. */
+        private final URI file;
+
+        /** An "average" value for the offset in each dimension, or {@code null} if unknown. */
+        private final double[] averages;
+
+        /** The factor by which to multiply each compressed value before to add to the average value. */
+        private final double scale;
+
+        /** Creates a new loader for the given file. */
+        Loader(final URI 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.
          *
-         *     GR3D  002024 024 20370201
-         *     GR3D1   -5.5000  10.0000  41.0000  52.0000    .1000    .1000
-         *     GR3D2 INTERPOLATION BILINEAIRE
-         *     GR3D3 PREC CM 01:5 02:10 03:20 04:50 99>100
+         * @return the loaded grid data.
+         * @throws FactoryException if an error occurred while loading the grid data.
          */
-        String line;
-        while (true) {
-            line = in.readLine();
-            if (line == null) {
-                throw new EOFException(Errors.format(Errors.Keys.UnexpectedEndOfFile_1, file));
-            }
-            final int length = CharSequences.skipTrailingWhitespaces(line, 0, line.length());
-            if (length <= 0) {
-                continue;   // Skip empty lines.
+        @Override
+        public DatumShiftGridFile<?,?> call() throws FactoryException {
+            final DatumShiftGridFile<?,?> grid;
+            try (BufferedReader in = newBufferedReader(file)) {
+                DatumShiftGridLoader.startLoading(FranceGeocentricInterpolation.class, file);
+                final DatumShiftGridFile.Float<Angle,Length> g = load(in, file);
+                grid = DatumShiftGridCompressed.compress(g, averages, scale);
+            } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
+                // NumberFormatException, ArithmeticException, NoSuchElementException, possibly other.
+                throw DatumShiftGridLoader.canNotLoad(HEADER, file, e);
             }
-            int p = CharSequences.skipLeadingWhitespaces(line, 0, length);
-            if (line.charAt(p) == '#') {
-                continue;   // Skip comment lines (not officially part of the format).
-            }
-            if (!line.regionMatches(true, p, HEADER, 0, HEADER.length())) {
-                break;      // End of header.
-            }
-            if ((p += HEADER.length()) < length) {
-                final char c = line.charAt(p);
-                p = CharSequences.skipLeadingWhitespaces(line, p+1, length);
-                switch (c) {
-                    case '1': {
-                        if (grid != null) {
-                            throw new FactoryException(Errors.format(Errors.Keys.DuplicatedElement_1, HEADER));
-                        }
-                        final double[] gridGeometry = CharSequences.parseDoubles(line.substring(p, length), ' ');
-                        if (gridGeometry.length == 6) {
-                            x0 = gridGeometry[0];
-                            xf = gridGeometry[1];
-                            y0 = gridGeometry[2];
-                            yf = gridGeometry[3];
-                            Δx = gridGeometry[4];
-                            Δy = gridGeometry[5];
-                            nx = Math.toIntExact(Math.round((xf - x0) / Δx + 1));
-                            ny = Math.toIntExact(Math.round((yf - y0) / Δy + 1));
-                            grid = new DatumShiftGridFile.Float<>(3,
-                                    Units.DEGREE, Units.METRE, false,
-                                    x0, y0, Δx, Δy, nx, ny, PARAMETERS, file);
-                            grid.accuracy = Double.NaN;
-                            for (final float[] data : grid.offsets) {
-                                Arrays.fill(data, Float.NaN);
+            return grid.useSharedData();
+        }
+
+        /**
+         * Unconditionally loads the grid for the given file without in-memory compression.
+         *
+         * @param  in    reader of the RGF93 datum shift file.
+         * @param  file  path to the file being read, used for parameter declaration and error reporting.
+         * @throws IOException if an I/O error occurred.
+         * @throws NumberFormatException if a number cannot be parsed.
+         * @throws NoSuchElementException if a data line is missing a value.
+         * @throws FactoryException if an problem is found with the file content.
+         * @throws ArithmeticException if the width or the height exceed the integer capacity.
+         */
+        static DatumShiftGridFile.Float<Angle,Length> load(final BufferedReader in, final URI file)
+                throws IOException, FactoryException, NoninvertibleTransformException
+        {
+            DatumShiftGridFile.Float<Angle,Length> grid = null;
+            double x0 = 0;
+            double xf = 0;
+            double y0 = 0;
+            double yf = 0;
+            double Δx = 0;
+            double Δy = 0;
+            int    nx = 0;
+            int    ny = 0;
+            /*
+             * The header should be like below, but the only essential line for this class is the one
+             * starting with "GR3D1". We also check that "GR3D2" declares the expected interpolation.
+             *
+             *     GR3D  002024 024 20370201
+             *     GR3D1   -5.5000  10.0000  41.0000  52.0000    .1000    .1000
+             *     GR3D2 INTERPOLATION BILINEAIRE
+             *     GR3D3 PREC CM 01:5 02:10 03:20 04:50 99>100
+             */
+            String line;
+            while (true) {
+                line = in.readLine();
+                if (line == null) {
+                    throw new EOFException(Errors.format(Errors.Keys.UnexpectedEndOfFile_1, file));
+                }
+                final int length = CharSequences.skipTrailingWhitespaces(line, 0, line.length());
+                if (length <= 0) {
+                    continue;   // Skip empty lines.
+                }
+                int p = CharSequences.skipLeadingWhitespaces(line, 0, length);
+                if (line.charAt(p) == '#') {
+                    continue;   // Skip comment lines (not officially part of the format).
+                }
+                if (!line.regionMatches(true, p, HEADER, 0, HEADER.length())) {
+                    break;      // End of header.
+                }
+                if ((p += HEADER.length()) < length) {
+                    final char c = line.charAt(p);
+                    p = CharSequences.skipLeadingWhitespaces(line, p+1, length);
+                    switch (c) {
+                        case '1': {
+                            if (grid != null) {
+                                throw new FactoryException(Errors.format(Errors.Keys.DuplicatedElement_1, HEADER));
+                            }
+                            final double[] gridGeometry = CharSequences.parseDoubles(line.substring(p, length), ' ');
+                            if (gridGeometry.length == 6) {
+                                x0 = gridGeometry[0];
+                                xf = gridGeometry[1];
+                                y0 = gridGeometry[2];
+                                yf = gridGeometry[3];
+                                Δx = gridGeometry[4];
+                                Δy = gridGeometry[5];
+                                nx = Math.toIntExact(Math.round((xf - x0) / Δx + 1));
+                                ny = Math.toIntExact(Math.round((yf - y0) / Δy + 1));
+                                grid = new DatumShiftGridFile.Float<>(3,
+                                        Units.DEGREE, Units.METRE, false,
+                                        x0, y0, Δx, Δy, nx, ny, PARAMETERS, file);
+                                grid.accuracy = Double.NaN;
+                                for (final float[] data : grid.offsets) {
+                                    Arrays.fill(data, Float.NaN);
+                                }
                             }
+                            break;
                         }
-                        break;
-                    }
-                    case '2': {
-                        final String interp = line.substring(p, length);
-                        if (!interp.matches("(?i)INTERPOLATION[^A-Z]+BILINEAIRE")) {
-                            final LogRecord record = Errors.getResources((Locale) null).getLogRecord(
-                                    Level.WARNING, Errors.Keys.UnsupportedInterpolation_1, interp);
-                            record.setLoggerName(Loggers.COORDINATE_OPERATION);
-                            Logging.log(FranceGeocentricInterpolation.class, "createMathTransform", record);
-                            // We declare `createMathTransform(…)` method because it is closer to public API.
+                        case '2': {
+                            final String interp = line.substring(p, length);
+                            if (!interp.matches("(?i)INTERPOLATION[^A-Z]+BILINEAIRE")) {
+                                final LogRecord record = Errors.getResources((Locale) null).getLogRecord(
+                                        Level.WARNING, Errors.Keys.UnsupportedInterpolation_1, interp);
+                                record.setLoggerName(Loggers.COORDINATE_OPERATION);
+                                Logging.log(FranceGeocentricInterpolation.class, "createMathTransform", record);
+                                // We declare `createMathTransform(…)` method because it is closer to public API.
+                            }
+                            break;
                         }
-                        break;
                     }
                 }
             }
-        }
-        if (grid == null) {
-            throw new FactoryException(Resources.format(Resources.Keys.FileNotFound_2, HEADER, file));
-        }
-        /*
-         * Loads the data with the sign of all offsets reversed. Data columns are
-         *
-         *     (unknown), longitude, latitude, tX, tY, tZ, accuracy code, data sheet (ignored)
-         *
-         * where the longitude and latitude values are in RGF93 system.
-         * Example:
-         *
-         *     00002   -5.500000000   41.000000000  -165.027   -67.100   315.813  99  -0158
-         *     00002   -5.500000000   41.100000000  -165.169   -66.948   316.007  99  -0157
-         *     00002   -5.500000000   41.200000000  -165.312   -66.796   316.200  99  -0157
-         *
-         * Translation values in the IGN file are from NTF to RGF93, but Apache SIS implementation needs
-         * the opposite direction (from RGF93 to NTF). The reason is that SIS expect the source datum to
-         * be the datum in which longitude and latitude values are expressed.
-         */
-        final float[] tX = grid.offsets[0];
-        final float[] tY = grid.offsets[1];
-        final float[] tZ = grid.offsets[2];
-        do {
-            final StringTokenizer t = new StringTokenizer(line.trim());
-            t.nextToken();                                                      // Ignored
-            final double x = Double.parseDouble(t.nextToken());                 // Longitude in degrees
-            final double y = Double.parseDouble(t.nextToken());                 // Latitude in degrees
-            final int    i = Math.toIntExact(Math.round((x - x0) / Δx));        // Column index
-            final int    j = Math.toIntExact(Math.round((y - y0) / Δy));        // Row index
-            if (i < 0 || i >= nx) {
-                throw new FactoryException(Errors.format(Errors.Keys.ValueOutOfRange_4, "x", x, x0, xf));
-            }
-            if (j < 0 || j >= ny) {
-                throw new FactoryException(Errors.format(Errors.Keys.ValueOutOfRange_4, "y", y, y0, yf));
-            }
-            final int p = j*nx + i;
-            if (!Double.isNaN(tX[p]) || !Double.isNaN(tY[p]) || !Double.isNaN(tZ[p])) {
-                throw new FactoryException(Errors.format(Errors.Keys.ValueAlreadyDefined_1, x + ", " + y));
-            }
-            tX[p] = -parseFloat(t.nextToken());     // See javadoc for the reason why we reverse the sign.
-            tY[p] = -parseFloat(t.nextToken());
-            tZ[p] = -parseFloat(t.nextToken());
-            final double accuracy = ACCURACY[Math.min(ACCURACY.length - 1,
-                    Math.max(0, Integer.parseInt(t.nextToken()) - 1))];
-            if (!(accuracy >= grid.accuracy)) {     // Use `!` for replacing the initial NaN.
-                grid.accuracy = accuracy;
+            if (grid == null) {
+                throw new FactoryException(Resources.format(Resources.Keys.FileNotFound_2, HEADER, file));
             }
-        } while ((line = in.readLine()) != null);
-        return grid;
+            /*
+             * Loads the data with the sign of all offsets reversed. Data columns are
+             *
+             *     (unknown), longitude, latitude, tX, tY, tZ, accuracy code, data sheet (ignored)
+             *
+             * where the longitude and latitude values are in RGF93 system.
+             * Example:
+             *
+             *     00002   -5.500000000   41.000000000  -165.027   -67.100   315.813  99  -0158
+             *     00002   -5.500000000   41.100000000  -165.169   -66.948   316.007  99  -0157
+             *     00002   -5.500000000   41.200000000  -165.312   -66.796   316.200  99  -0157
+             *
+             * Translation values in the IGN file are from NTF to RGF93, but Apache SIS implementation needs
+             * the opposite direction (from RGF93 to NTF). The reason is that SIS expect the source datum to
+             * be the datum in which longitude and latitude values are expressed.
+             */
+            final float[] tX = grid.offsets[0];
+            final float[] tY = grid.offsets[1];
+            final float[] tZ = grid.offsets[2];
+            do {
+                final StringTokenizer t = new StringTokenizer(line.trim());
+                t.nextToken();                                                      // Ignored
+                final double x = Double.parseDouble(t.nextToken());                 // Longitude in degrees
+                final double y = Double.parseDouble(t.nextToken());                 // Latitude in degrees
+                final int    i = Math.toIntExact(Math.round((x - x0) / Δx));        // Column index
+                final int    j = Math.toIntExact(Math.round((y - y0) / Δy));        // Row index
+                if (i < 0 || i >= nx) {
+                    throw new FactoryException(Errors.format(Errors.Keys.ValueOutOfRange_4, "x", x, x0, xf));
+                }
+                if (j < 0 || j >= ny) {
+                    throw new FactoryException(Errors.format(Errors.Keys.ValueOutOfRange_4, "y", y, y0, yf));
+                }
+                final int p = j*nx + i;
+                if (!Double.isNaN(tX[p]) || !Double.isNaN(tY[p]) || !Double.isNaN(tZ[p])) {
+                    throw new FactoryException(Errors.format(Errors.Keys.ValueAlreadyDefined_1, x + ", " + y));
+                }
+                tX[p] = -parseFloat(t.nextToken());     // See javadoc for the reason why we reverse the sign.
+                tY[p] = -parseFloat(t.nextToken());
+                tZ[p] = -parseFloat(t.nextToken());
+                final double accuracy = ACCURACY[Math.min(ACCURACY.length - 1,
+                        Math.max(0, Integer.parseInt(t.nextToken()) - 1))];
+                if (!(accuracy >= grid.accuracy)) {     // Use `!` for replacing the initial NaN.
+                    grid.accuracy = accuracy;
+                }
+            } while ((line = in.readLine()) != null);
+            return grid;
+        }
     }
 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MolodenskyInterpolation.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MolodenskyInterpolation.java
index 33f3eb118f..e74f6274fe 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MolodenskyInterpolation.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/MolodenskyInterpolation.java
@@ -47,6 +47,7 @@ import org.apache.sis.referencing.operation.transform.InterpolatedMolodenskyTran
  */
 @XmlTransient
 @Deprecated(since="1.4", forRemoval=true)
+// Note: after removal, delete overrideable method in parent class.
 public final class MolodenskyInterpolation extends FranceGeocentricInterpolation {
     /**
      * Serial number for inter-operability with different versions.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
index 0828872fd7..e50e10723d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NADCON.java
@@ -16,14 +16,11 @@
  */
 package org.apache.sis.internal.referencing.provider;
 
-import java.util.AbstractMap;
 import java.io.IOException;
+import java.net.URI;
 import java.nio.ByteOrder;
 import java.nio.ByteBuffer;
 import java.nio.FloatBuffer;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.Files;
 import java.nio.channels.ReadableByteChannel;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.measure.quantity.Angle;
@@ -40,9 +37,7 @@ import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.parameter.Parameters;
 import org.apache.sis.util.CharSequences;
-import org.apache.sis.util.collection.Cache;
 import org.apache.sis.util.resources.Errors;
-import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.measure.Units;
 
 
@@ -56,7 +51,7 @@ import org.apache.sis.measure.Units;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rueben Schulz (UBC)
- * @version 1.3
+ * @version 1.4
  *
  * @see <a href="http://www.ngs.noaa.gov/cgi-bin/nadcon.prl">NADCON on-line computation</a>
  *
@@ -83,7 +78,7 @@ public final class NADCON extends AbstractProvider {
      *   <li>Default value: {@code conus.las}</li>
      * </ul>
      */
-    private static final ParameterDescriptor<Path> LATITUDE;
+    private static final ParameterDescriptor<URI> LATITUDE;
 
     /**
      * The operation parameter descriptor for the <cite>"Longitude difference file"</cite> parameter value.
@@ -99,7 +94,7 @@ public final class NADCON extends AbstractProvider {
      *   <li>Default value: {@code conus.los}</li>
      * </ul>
      */
-    private static final ParameterDescriptor<Path> LONGITUDE;
+    private static final ParameterDescriptor<URI> LONGITUDE;
 
     /**
      * The group of all parameters expected by this coordinate operation.
@@ -110,11 +105,11 @@ public final class NADCON extends AbstractProvider {
         LATITUDE = builder
                 .addIdentifier("8657")
                 .addName("Latitude difference file")
-                .create(Path.class, Paths.get("conus.las"));
+                .create(URI.class, URI.create("conus.las"));
         LONGITUDE = builder
                 .addIdentifier("8658")
                 .addName("Longitude difference file")
-                .create(Path.class, Paths.get("conus.los"));
+                .create(URI.class, URI.create("conus.los"));
         PARAMETERS = builder
                 .addIdentifier("9613")
                 .addName("NADCON")
@@ -149,52 +144,42 @@ public final class NADCON extends AbstractProvider {
     }
 
     /**
-     * Returns the grid of the given name. This method returns the cached instance if it still exists,
-     * or load the grid otherwise.
+     * Returns the grid of the given name.
+     * This method returns the cached instance if it still exists, or load the grid otherwise.
      *
-     * @param latitudeShifts   name of the grid file for latitude shifts.
-     * @param longitudeShifts  name of the grid file for longitude shifts.
+     * @param latitudeShifts   relative or absolute path of the grid file for latitude shifts.
+     * @param longitudeShifts  relative or absolute path name of the grid file for longitude shifts.
      */
-    static DatumShiftGridFile<Angle,Angle> getOrLoad(final Path latitudeShifts, final Path longitudeShifts)
+    static DatumShiftGridFile<Angle,Angle> getOrLoad(final URI latitudeShifts, final URI longitudeShifts)
             throws FactoryException
     {
-        final Path rlat = DataDirectory.DATUM_CHANGES.resolve(latitudeShifts).toAbsolutePath();
-        final Path rlon = DataDirectory.DATUM_CHANGES.resolve(longitudeShifts).toAbsolutePath();
-        final Object key = new AbstractMap.SimpleImmutableEntry<>(rlat, rlon);
-        DatumShiftGridFile<?,?> grid = DatumShiftGridFile.CACHE.peek(key);
-        if (grid == null) {
-            final Cache.Handler<DatumShiftGridFile<?,?>> handler = DatumShiftGridFile.CACHE.lock(key);
+        final URI rlat = Loader.toAbsolutePath(latitudeShifts);
+        final URI rlon = Loader.toAbsolutePath(longitudeShifts);
+        return DatumShiftGridFile.getOrLoad(rlat, rlon, () -> {
+            final Loader loader;
+            URI file = latitudeShifts;
+            final DatumShiftGridFile<?,?> grid;
             try {
-                grid = handler.peek();
-                if (grid == null) {
-                    final Loader loader;
-                    Path file = latitudeShifts;
-                    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 = Files.newByteChannel(rlat)) {
-                            DatumShiftGridLoader.startLoading(NADCON.class, CharSequences.commonPrefix(
-                                    latitudeShifts.toString(), longitudeShifts.toString()).toString() + '…');
-                            loader = new Loader(in, buffer, file);
-                            loader.readGrid(fb, null, longitudeShifts);
-                        }
-                        buffer.clear();
-                        file = longitudeShifts;
-                        try (ReadableByteChannel in = Files.newByteChannel(rlon)) {
-                            new Loader(in, buffer, file).readGrid(fb, loader, null);
-                        }
-                    } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
-                        throw DatumShiftGridLoader.canNotLoad("NADCON", file, e);
-                    }
-                    grid = DatumShiftGridCompressed.compress(loader.grid, null, loader.grid.accuracy);
-                    grid = grid.useSharedData();
+                // 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 = Loader.newByteChannel(rlat)) {
+                    DatumShiftGridLoader.startLoading(NADCON.class, CharSequences.commonPrefix(
+                            latitudeShifts.toString(), longitudeShifts.toString()).toString() + '…');
+                    loader = new Loader(in, buffer, file);
+                    loader.readGrid(fb, null, longitudeShifts);
                 }
-            } finally {
-                handler.putAndUnlock(grid);
+                buffer.clear();
+                file = longitudeShifts;
+                try (ReadableByteChannel in = Loader.newByteChannel(rlon)) {
+                    new Loader(in, buffer, file).readGrid(fb, loader, null);
+                }
+            } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
+                throw DatumShiftGridLoader.canNotLoad("NADCON", file, e);
             }
-        }
-        return grid.castTo(Angle.class, Angle.class);
+            grid = DatumShiftGridCompressed.compress(loader.grid, null, loader.grid.accuracy);
+            return grid.useSharedData();
+        }).castTo(Angle.class, Angle.class);
     }
 
 
@@ -260,7 +245,7 @@ public final class NADCON extends AbstractProvider {
         private final StringBuilder ascii;
 
         /**
-         * The grid created by {@link #readGrid(FloatBuffer, Loader, Path)}.
+         * The grid created by {@link #readGrid(FloatBuffer, Loader, URI)}.
          */
         DatumShiftGridFile.Float<Angle,Angle> grid;
 
@@ -273,7 +258,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 Path file)
+        Loader(final ReadableByteChannel channel, final ByteBuffer buffer, final URI file)
                 throws IOException, FactoryException
         {
             super(channel, buffer, file);
@@ -387,7 +372,7 @@ public final class NADCON extends AbstractProvider {
          * @param latitudeShifts   the previously loaded latitude shifts, or {@code null} if not yet loaded.
          * @param longitudeShifts  the file for the longitude grid.
          */
-        final void readGrid(final FloatBuffer fb, final Loader latitudeShifts, final Path longitudeShifts)
+        final void readGrid(final FloatBuffer fb, final Loader latitudeShifts, final URI longitudeShifts)
                 throws IOException, FactoryException, NoninvertibleTransformException
         {
             final int dim;
@@ -403,7 +388,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.getFileName(), file.getFileName()));
+                            latitudeShifts.file.getPath(), file.getPath()));
                 }
                 dim   = 0;                                              // Dimension of longitudes
                 scale = -DEGREES_TO_SECONDS * Δx;                       // NADCON shifts are positive west.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
index 108d96fe9e..a44fb3a054 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/NTv2.java
@@ -25,10 +25,9 @@ import java.util.Arrays;
 import java.util.Locale;
 import java.util.logging.Level;
 import java.io.IOException;
+import java.net.URI;
 import java.nio.ByteOrder;
 import java.nio.ByteBuffer;
-import java.nio.file.Files;
-import java.nio.file.Path;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.charset.StandardCharsets;
 import javax.xml.bind.annotation.XmlTransient;
@@ -44,13 +43,11 @@ import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.MathTransformFactory;
 import org.opengis.referencing.operation.Transformation;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
-import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.internal.referencing.Formulas;
 import org.apache.sis.internal.referencing.Resources;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.parameter.Parameters;
-import org.apache.sis.util.collection.Cache;
 import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Messages;
@@ -63,7 +60,7 @@ import org.apache.sis.measure.Units;
  *
  * @author  Simon Reynard (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.3
+ * @version 1.4
  * @since   0.7
  */
 @XmlTransient
@@ -87,7 +84,7 @@ public final class NTv2 extends AbstractProvider {
      *   <li>No default value</li>
      * </ul>
      */
-    static final ParameterDescriptor<Path> FILE;
+    static final ParameterDescriptor<URI> FILE;
 
     /**
      * The group of all parameters expected by this coordinate operation.
@@ -98,7 +95,7 @@ public final class NTv2 extends AbstractProvider {
         FILE = builder
                 .addIdentifier("8656")
                 .addName("Latitude and longitude difference file")
-                .create(Path.class, null);
+                .create(URI.class, null);
         PARAMETERS = builder
                 .addIdentifier("9615")
                 .addName("NTv2")
@@ -151,38 +148,29 @@ public final class NTv2 extends AbstractProvider {
     }
 
     /**
-     * Returns the grid of the given name. This method returns the cached instance if it still exists,
-     * or load the grid otherwise.
+     * Returns the grid of the given name.
+     * This method returns the cached instance if it still exists, or load the grid otherwise.
      *
      * @param  provider  the provider which is creating a transform.
-     * @param  file      name of the datum shift grid file to load.
+     * @param  file      relative or absolute path of the datum shift grid file to load.
      * @param  version   the expected version (1 or 2).
      */
     static DatumShiftGridFile<Angle,Angle> getOrLoad(final Class<? extends AbstractProvider> provider,
-            final Path file, final int version) throws FactoryException
+            final URI file, final int version) throws FactoryException
     {
-        final Path resolved = DataDirectory.DATUM_CHANGES.resolve(file).toAbsolutePath();
-        DatumShiftGridFile<?,?> grid = DatumShiftGridFile.CACHE.peek(resolved);
-        if (grid == null) {
-            final Cache.Handler<DatumShiftGridFile<?,?>> handler = DatumShiftGridFile.CACHE.lock(resolved);
-            try {
-                grid = handler.peek();
-                if (grid == null) {
-                    try (ReadableByteChannel in = Files.newByteChannel(resolved)) {
-                        DatumShiftGridLoader.startLoading(provider, file);
-                        final Loader loader = new Loader(in, file, version);
-                        grid = loader.readAllGrids();
-                        loader.report(provider);
-                    } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
-                        throw DatumShiftGridLoader.canNotLoad(provider.getSimpleName(), file, e);
-                    }
-                    grid = grid.useSharedData();
-                }
-            } finally {
-                handler.putAndUnlock(grid);
+        final URI resolved = Loader.toAbsolutePath(file);
+        return DatumShiftGridFile.getOrLoad(resolved, null, () -> {
+            final DatumShiftGridFile<?,?> grid;
+            try (ReadableByteChannel in = Loader.newByteChannel(resolved)) {
+                DatumShiftGridLoader.startLoading(provider, file);
+                final Loader loader = new Loader(in, file, version);
+                grid = loader.readAllGrids();
+                loader.report(provider);
+            } catch (IOException | NoninvertibleTransformException | RuntimeException e) {
+                throw DatumShiftGridLoader.canNotLoad(provider.getSimpleName(), file, e);
             }
-        }
-        return grid.castTo(Angle.class, Angle.class);
+            return grid.useSharedData();
+        }).castTo(Angle.class, Angle.class);
     }
 
 
@@ -321,7 +309,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 Path file, int version) throws IOException, FactoryException {
+        Loader(final ReadableByteChannel channel, final URI file, int version) throws IOException, FactoryException {
             super(channel, ByteBuffer.allocate(4096), file);
             header = new LinkedHashMap<>();
             ensureBufferContains(RECORD_LENGTH);
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/DatumShiftTestCase.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/DatumShiftTestCase.java
index 4139f4b352..13cb9d581d 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/DatumShiftTestCase.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/DatumShiftTestCase.java
@@ -16,11 +16,9 @@
  */
 package org.apache.sis.internal.referencing.provider;
 
+import java.net.URI;
 import java.net.URL;
 import java.net.URISyntaxException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.FileSystemNotFoundException;
 import org.apache.sis.test.TestCase;
 
 import static org.junit.Assert.*;
@@ -30,10 +28,10 @@ import static org.junit.Assume.assumeFalse;
 /**
  * Base class of tests that need to load a datum shift grid. This base class provides a
  * {@link #getResourceAsConvertibleURL(String)} method for fetching the data in a form
- * convertible to {@link Path}.
+ * convertible to {@link URI}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.4
  * @since   0.8
  */
 public abstract class DatumShiftTestCase extends TestCase {
@@ -44,7 +42,7 @@ public abstract class DatumShiftTestCase extends TestCase {
     }
 
     /**
-     * Finds resource of the given name as a URL convertible to a {@link Path}.
+     * Finds resource of the given name as a URL convertible to a {@link URI}.
      * If the URL is not convertible, then this method declares the test as ignored.
      *
      * @param  name  name of the resource to get.
@@ -61,22 +59,17 @@ public abstract class DatumShiftTestCase extends TestCase {
     }
 
     /**
-     * Finds resource of the given name as a path. If the resource cannot be obtained because
+     * Finds resource of the given name as an URI. If the resource cannot be obtained because
      * the grid file is inside a JAR file, declares the test as ignored instead of failed.
      *
      * @param  name  name of the resource to get.
      * @return the requested resources.
      */
-    static Path getResource(final String name) throws URISyntaxException {
-        final URL file = DatumShiftTestCase.class.getResource(name);
+    static URI getResource(final String name) throws URISyntaxException {
+        final URL file = getResourceAsConvertibleURL(name);
         if (file == null) {
-            fail("Test file \"" + name + "\" not found.");
-        } else try {
-            return Paths.get(file.toURI());
-        } catch (FileSystemNotFoundException e) {
             assumeFalse("Cannot read grid data in a JAR file.", "jar".equals(file.getProtocol()));
-            throw e;
         }
-        return null;
+        return file.toURI();
     }
 }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolationTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolationTest.java
index 41917c679b..2dc409a8d5 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolationTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/FranceGeocentricInterpolationTest.java
@@ -19,9 +19,7 @@ package org.apache.sis.internal.referencing.provider;
 import java.net.URISyntaxException;
 import java.io.BufferedReader;
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.nio.file.Path;
+import java.net.URI;
 import javax.measure.quantity.Angle;
 import javax.measure.quantity.Length;
 import org.opengis.geometry.Envelope;
@@ -38,7 +36,7 @@ import static org.opengis.test.Assert.*;
  * Tests {@link FranceGeocentricInterpolation}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.4
  *
  * @see GeocentricTranslationTest#testFranceGeocentricInterpolationPoint()
  * @see org.apache.sis.referencing.operation.transform.MolodenskyTransformTest#testFranceGeocentricInterpolationPoint()
@@ -90,14 +88,16 @@ public final class FranceGeocentricInterpolationTest extends DatumShiftTestCase
     public static final double ANGULAR_TOLERANCE = (0.0001 / 60 / 60) / 2;
 
     /**
-     * Tests {@link FranceGeocentricInterpolation#isRecognized(Path)}.
+     * Tests {@link FranceGeocentricInterpolation#isRecognized(URI)}.
+     *
+     * @throws URISyntaxException if the URL to the test file is not valid.
      */
     @Test
-    public void testIsRecognized() {
-        assertTrue (FranceGeocentricInterpolation.isRecognized(Paths.get("GR3DF97A.txt")));
-        assertTrue (FranceGeocentricInterpolation.isRecognized(Paths.get("gr3df")));
-        assertFalse(FranceGeocentricInterpolation.isRecognized(Paths.get("gr3d")));
-        assertTrue (FranceGeocentricInterpolation.isRecognized(Paths.get(TEST_FILE)));
+    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)));
     }
 
     /**
@@ -111,7 +111,7 @@ public final class FranceGeocentricInterpolationTest extends DatumShiftTestCase
     /**
      * Tests a small grid file with interpolations in geocentric coordinates.
      *
-     * @throws URISyntaxException if the URL to the test file cannot be converted to a path.
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws IOException if an error occurred while loading the grid.
      * @throws FactoryException if an error occurred while computing the grid.
      * @throws TransformException if an error occurred while computing the envelope.
@@ -128,7 +128,7 @@ public final class FranceGeocentricInterpolationTest extends DatumShiftTestCase
      * The next method is {@link #testGridAsShorts(DatumShiftGridFile)}.</p>
      *
      * @return the loaded grid with values as {@code float}.
-     * @throws URISyntaxException if the URL to the test file cannot be converted to a path.
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws IOException if an error occurred while loading the grid.
      * @throws FactoryException if an error occurred while computing the grid.
      * @throws TransformException if an error occurred while computing the envelope.
@@ -137,10 +137,10 @@ public final class FranceGeocentricInterpolationTest extends DatumShiftTestCase
     private static DatumShiftGridFile<Angle,Length> testGridAsFloats()
             throws URISyntaxException, IOException, FactoryException, TransformException
     {
-        final Path file = getResource(TEST_FILE);
+        final URI file = getResource(TEST_FILE);
         final DatumShiftGridFile.Float<Angle,Length> grid;
-        try (BufferedReader in = Files.newBufferedReader(file)) {
-            grid = FranceGeocentricInterpolation.load(in, file);
+        try (BufferedReader in = FranceGeocentricInterpolation.Loader.newBufferedReader(file)) {
+            grid = FranceGeocentricInterpolation.Loader.load(in, file);
         }
         assertEquals("cellPrecision",   0.005, grid.getCellPrecision(), STRICT);
         assertEquals("getCellMean",  168.2587, grid.getCellMean(0), 0.0001);
@@ -219,9 +219,9 @@ public final class FranceGeocentricInterpolationTest extends DatumShiftTestCase
     }
 
     /**
-     * Tests the {@link FranceGeocentricInterpolation#getOrLoad(Path, double[], double)} method and its cache.
+     * Tests the {@link FranceGeocentricInterpolation#getOrLoad(URI, double[], double)} method and its cache.
      *
-     * @throws URISyntaxException if the URL to the test file cannot be converted to a path.
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws FactoryException if an error occurred while computing the grid.
      * @throws TransformException if an error occurred while computing the envelope.
      */
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NADCONTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NADCONTest.java
index 7a03e51bc3..c159b4e9f4 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NADCONTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NADCONTest.java
@@ -19,6 +19,7 @@ package org.apache.sis.internal.referencing.provider;
 import java.util.Locale;
 import java.io.BufferedWriter;
 import java.io.IOException;
+import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.nio.file.Files;
@@ -40,7 +41,7 @@ import static org.opengis.test.Assert.*;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Simon Reynard (Geomatys)
- * @version 0.8
+ * @version 1.4
  * @since   0.7
  */
 public final class NADCONTest extends DatumShiftTestCase {
@@ -91,7 +92,7 @@ public final class NADCONTest extends DatumShiftTestCase {
      * Tests loading a grid file and interpolating a sample point.
      * The point used for this test is given by {@link #samplePoint(int)}.
      *
-     * @throws URISyntaxException if the URL to the test file cannot be converted to a path.
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws FactoryException if an error occurred while loading or computing the grid.
      * @throws TransformException if an error occurred while computing the envelope or testing the point.
      */
@@ -113,21 +114,21 @@ public final class NADCONTest extends DatumShiftTestCase {
      * @throws FactoryException if an error occurred while loading or computing the grid.
      * @throws TransformException if an error occurred while computing the envelope or testing the point.
      */
-    public static void testNADCON(final Path latitudeShifts, final Path longitudeShifts)
+    public static void testNADCON(final URI latitudeShifts, final URI longitudeShifts)
             throws FactoryException, TransformException
     {
         testNADCON(latitudeShifts, longitudeShifts, -131, -63, 20, 50);
     }
 
     /**
-     * Implementation of {@link #testLoader()} and {@link #testNADCON(Path, Path)}.
+     * Implementation of {@link #testLoader()} and {@link #testNADCON(URI, URI)}.
      *
      * @param  xmin  westmost longitude.
      * @param  xmax  eastmost longitude.
      * @param  ymin  southmost latitude.
      * @param  ymax  northmost latitude.
      */
-    private static void testNADCON(final Path latitudeShifts, final Path longitudeShifts,
+    private static void testNADCON(final URI latitudeShifts, final URI longitudeShifts,
             final double xmin, final double xmax, final double ymin, final double ymax)
             throws FactoryException, TransformException
     {
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NTv2Test.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NTv2Test.java
index c3e6299551..82a2b93f1a 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NTv2Test.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/provider/NTv2Test.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.internal.referencing.provider;
 
+import java.net.URI;
 import java.net.URISyntaxException;
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -35,7 +36,6 @@ import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.measure.Units;
 import org.apache.sis.internal.referencing.Formulas;
-import org.apache.sis.internal.system.DataDirectory;
 import org.apache.sis.test.DependsOn;
 import org.junit.Test;
 
@@ -49,7 +49,7 @@ import static org.apache.sis.internal.referencing.provider.DatumShiftGridLoader.
  * It will also indirectly tests {@link DatumShiftGridGroup} class.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  *
  * @see GeocentricTranslationTest#testFranceGeocentricInterpolationPoint()
  * @see org.apache.sis.referencing.operation.transform.MolodenskyTransformTest#testFranceGeocentricInterpolationPoint()
@@ -80,7 +80,7 @@ public final class NTv2Test extends DatumShiftTestCase {
      * Tests loading a grid file and interpolating a sample point. The point used for
      * this test is given by {@link FranceGeocentricInterpolationTest#samplePoint(int)}.
      *
-     * @throws URISyntaxException if the URL to the test file cannot be converted to a path.
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws IOException if an error occurred while loading the grid.
      * @throws FactoryException if an error occurred while computing the grid.
      * @throws TransformException if an error occurred while computing the envelope or testing the point.
@@ -104,19 +104,19 @@ public final class NTv2Test extends DatumShiftTestCase {
      * @throws FactoryException if an error occurred while loading or computing the grid.
      * @throws TransformException if an error occurred while computing the envelope or testing the point.
      */
-    public static void testRGF93(final Path file) throws FactoryException, TransformException {
+    public static void testRGF93(final URI file) throws FactoryException, TransformException {
         testRGF93(file, -19800, 36000, 147600, 187200);
     }
 
     /**
-     * Implementation of {@link #testLoader()} and {@link #testRGF93(Path)}.
+     * Implementation of {@link #testLoader()} and {@link #testRGF93(URI)}.
      *
      * @param  xmin  negative of value of {@code "W_LONG"} record.
      * @param  xmax  negative of value of {@code "E_LONG"} record.
      * @param  ymin  value of the {@code "S_LAT"} record.
      * @param  ymax  value of the {@code "N_LAT"} record.
      */
-    private static void testRGF93(final Path file, final double xmin, final double xmax,
+    private static void testRGF93(final URI file, final double xmin, final double xmax,
             final double ymin, final double ymax) throws FactoryException, TransformException
     {
         final double cellSize = 360;
@@ -177,14 +177,15 @@ public final class NTv2Test extends DatumShiftTestCase {
      * to be present in the {@code $SIS_DATA/DatumChanges} directory. This test is executed only if the
      * {@link #RUN_EXTENSIVE_TESTS} flag is set.
      *
+     * @throws URISyntaxException if the URL to the test file is not valid.
      * @throws FactoryException if an error occurred while loading or computing the grid.
      * @throws TransformException if an error occurred while computing the envelope or testing the point.
      */
     @Test
-    public void testMultiGrids() throws FactoryException, TransformException {
+    public void testMultiGrids() throws URISyntaxException, FactoryException, TransformException {
         assumeTrue(RUN_EXTENSIVE_TESTS);
-        final Path file = DataDirectory.DATUM_CHANGES.resolve(Paths.get(MULTIGRID_TEST_FILE));
-        assumeTrue(Files.exists(file));
+        final URI file = DatumShiftGridLoader.toAbsolutePath(new URI(MULTIGRID_TEST_FILE));
+        assumeTrue(Files.exists(Paths.get(file)));
         final DatumShiftGridFile<Angle,Angle> grid = NTv2.getOrLoad(NTv2.class, file, 2);
         assertInstanceOf("Should contain many grids.", DatumShiftGridGroup.class, grid);
         assertEquals("coordinateUnit",  Units.ARC_SECOND, grid.getCoordinateUnit());
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/DatumShiftTest.java b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/DatumShiftTest.java
index 304ff3dcdb..38b116d3e8 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/integration/DatumShiftTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/integration/DatumShiftTest.java
@@ -16,7 +16,7 @@
  */
 package org.apache.sis.test.integration;
 
-import java.nio.file.Path;
+import java.net.URI;
 import java.io.IOException;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.operation.TransformException;
@@ -37,7 +37,7 @@ import static org.apache.sis.test.Assume.*;
  * directory.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.4
  * @since   0.7
  */
 @DependsOn({
@@ -56,7 +56,7 @@ public final class DatumShiftTest extends TestCase {
      */
     @Test
     public void testRGF93() throws IOException, FactoryException, TransformException {
-        final Path file = assumeDataExists(DataDirectory.DATUM_CHANGES, "ntf_r93.gsb");
+        final URI file = assumeDataExists(DataDirectory.DATUM_CHANGES, "ntf_r93.gsb");
         NTv2Test.testRGF93(file);
     }
 
@@ -70,8 +70,8 @@ public final class DatumShiftTest extends TestCase {
      */
     @Test
     public void testNADCON() throws IOException, FactoryException, TransformException {
-        final Path latitudeShifts  = assumeDataExists(DataDirectory.DATUM_CHANGES, "conus.las");
-        final Path longitudeShifts = assumeDataExists(DataDirectory.DATUM_CHANGES, "conus.los");
+        final URI latitudeShifts  = assumeDataExists(DataDirectory.DATUM_CHANGES, "conus.las");
+        final URI longitudeShifts = assumeDataExists(DataDirectory.DATUM_CHANGES, "conus.los");
         NADCONTest.testNADCON(latitudeShifts, longitudeShifts);
     }
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java b/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java
index 32746aa757..641bd5b7ac 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/system/DataDirectory.java
@@ -33,7 +33,7 @@ import static java.util.logging.Logger.getLogger;
  * Sub-directories of {@code SIS_DATA} where SIS looks for EPSG database, datum shift grids and other resources.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.4
  * @since   0.7
  */
 public enum DataDirectory {
@@ -228,25 +228,4 @@ public enum DataDirectory {
         }
         return directory;
     }
-
-    /**
-     * If the given path is relative, returns the path as a child of the directory represented by this enum.
-     * If no valid directory is configured by the {@code SIS_DATA} environment variable, then the relative
-     * path is returned as-is.
-     *
-     * <p>This method is invoked for files that may be user-specified, for example datum shift file specified
-     * in {@link org.opengis.parameter.ParameterValue}.</p>
-     *
-     * @param  file  the path to resolve, or {@code null}.
-     * @return the path to use, or {@code null} if the given path was null.
-     */
-    public Path resolve(Path file) {
-        if (file != null && !file.isAbsolute()) {
-            final Path dir = getDirectory();
-            if (dir != null) {
-                return dir.resolve(file);
-            }
-        }
-        return file;
-    }
 }
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/Assume.java b/core/sis-utility/src/test/java/org/apache/sis/test/Assume.java
index 6b3db4f30c..efe64d3fcd 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/Assume.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/Assume.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.test;
 
+import java.net.URI;
 import java.nio.file.Path;
 import java.nio.file.Files;
 import org.apache.sis.internal.system.DataDirectory;
@@ -25,7 +26,7 @@ import org.apache.sis.internal.system.DataDirectory;
  * Assumption methods used by the SIS project in addition of the JUnit ones.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.4
  * @since   0.7
  */
 public final class Assume extends org.junit.Assume {
@@ -48,13 +49,13 @@ public final class Assume extends org.junit.Assume {
      * @param  file  the file that needs to exist.
      * @return the path to the given file.
      */
-    public static Path assumeDataExists(final DataDirectory type, final String file) {
+    public static URI assumeDataExists(final DataDirectory type, final String file) {
         assumeNotNull("$SIS_DATA environment variable not set.", System.getenv(DataDirectory.ENV));
         Path path = type.getDirectory();
         assumeNotNull("$SIS_DATA/" + type + " directory not found.", path);
         path = path.resolve(file);
         assumeTrue("Specified file or directory not found.", Files.exists(path));
         assumeTrue("Specified directory not readable.", Files.isReadable(path));
-        return path;
+        return path.toUri();
     }
 }
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/package-info.java b/core/sis-utility/src/test/java/org/apache/sis/test/package-info.java
index 537edf9e95..8bf3aab850 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/package-info.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/package-info.java
@@ -41,7 +41,7 @@
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.4
  * @since   0.3
  */
 package org.apache.sis.test;