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 9beb499ee7bfed73abfcde27e63b4db74a4f62a1
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed Dec 27 16:17:41 2023 +0100

    When parsing a GML or WKT from a URI, store the URI on a new 
`DefaultParameterValue.sourceFile` property.
    This information allows to resolve `ParameterValue.valueFile` relatively to 
the source document.
    
    https://issues.apache.org/jira/browse/SIS-593
---
 .../apache/sis/xml/util/ExternalLinkHandler.java   |   3 +-
 .../main/org/apache/sis/io/wkt/AbstractParser.java |  23 ++-
 .../apache/sis/io/wkt/GeodeticObjectParser.java    |  11 +-
 .../org/apache/sis/io/wkt/MathTransformParser.java |  20 ++-
 .../main/org/apache/sis/io/wkt/Parser.java         |   4 +-
 .../main/org/apache/sis/io/wkt/WKTFormat.java      | 129 ++++++++++------
 .../sis/parameter/AbstractParameterDescriptor.java |  13 +-
 .../sis/parameter/DefaultParameterDescriptor.java  |  15 +-
 .../parameter/DefaultParameterDescriptorGroup.java |  15 +-
 .../sis/parameter/DefaultParameterValue.java       | 171 ++++++++++++++++-----
 .../sis/parameter/UnmodifiableParameterValue.java  |  10 ++
 .../org/apache/sis/parameter/package-info.java     |   2 +-
 .../main/org/apache/sis/referencing/CRS.java       |   7 +-
 .../referencing/factory/GeodeticObjectFactory.java |   8 +-
 .../transform/DefaultMathTransformFactory.java     |   8 +-
 .../test/org/apache/sis/io/wkt/ElementTest.java    |   4 +-
 .../sis/io/wkt/GeodeticObjectParserTest.java       |   4 +-
 .../org/apache/sis/storage/base/PRJDataStore.java  |  21 ++-
 18 files changed, 315 insertions(+), 153 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java
index 6bd949f064..6bb7082bc7 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java
@@ -132,7 +132,7 @@ public class ExternalLinkHandler {
         if (b == null) {
             return null;
         }
-        final URI baseURI;
+        URI baseURI;
         if (b instanceof URI) {         // `instanceof` check of final classes 
are efficient.
             baseURI = (URI) b;
         } else {
@@ -151,6 +151,7 @@ public class ExternalLinkHandler {
                 warningOccured(b, e);
                 return null;
             }
+            baseURI = baseURI.normalize();
             base = baseURI;
         }
         return baseURI;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java
index 439d33516c..03b137c7b2 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/AbstractParser.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.io.wkt;
 
+import java.net.URI;
 import java.util.Map;
 import java.util.List;
 import java.util.Date;
@@ -35,11 +36,10 @@ import org.opengis.util.FactoryException;
 import org.opengis.util.InternationalString;
 import org.apache.sis.system.Loggers;
 import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.internal.StandardDateFormat;
 import org.apache.sis.measure.Units;
 import org.apache.sis.measure.UnitFormat;
-import org.apache.sis.util.resources.Errors;
-import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
 
 
 /**
@@ -83,6 +83,13 @@ abstract class AbstractParser implements Parser {
      */
     static final int MANDATORY = 2;
 
+    /**
+     * The URI to declare as the source of the WKT definitions, or {@code 
null} if unknown.
+     * This information is not used directly by the parser, but will be stored 
in parameter values
+     * as a hint for resolving relative paths as absolute paths.
+     */
+    final URI sourceFile;
+
     /**
      * The locale for formatting error messages if parsing fails, or {@code 
null} for system default.
      * This is <strong>not</strong> the locale for parsing number or date 
values.
@@ -151,22 +158,24 @@ abstract class AbstractParser implements Parser {
     /**
      * Constructs a parser using the specified set of symbols.
      *
-     * @param  symbols       the set of symbols to use.
+     * @param  sourceFile    URI to declare as the source of the WKT 
definitions, or {@code null} if unknown.
      * @param  fragments     reference to the {@link WKTFormat#fragments} map, 
or an empty map if none.
+     * @param  symbols       the set of symbols to use. Cannot be null.
      * @param  numberFormat  the number format provided by {@link WKTFormat}, 
or {@code null} for a default format.
      * @param  dateFormat    the date format provided by {@link WKTFormat}, or 
{@code null} for a default format.
      * @param  unitFormat    the unit format provided by {@link WKTFormat}, or 
{@code null} for a default format.
      * @param  errorLocale   the locale for error messages (not for parsing), 
or {@code null} for the system default.
      */
-    AbstractParser(final Symbols symbols, final Map<String,StoredTree> 
fragments, NumberFormat numberFormat,
-                   final DateFormat dateFormat, final UnitFormat unitFormat, 
final Locale errorLocale)
+    AbstractParser(final URI sourceFile, final Map<String,StoredTree> 
fragments, final Symbols symbols,
+                   NumberFormat numberFormat, final DateFormat dateFormat, 
final UnitFormat unitFormat,
+                   final Locale errorLocale)
     {
-        ensureNonNull("symbols", symbols);
         if (numberFormat == null) {
             numberFormat = symbols.createNumberFormat();
         }
-        this.symbols     = symbols;
+        this.sourceFile  = sourceFile;
         this.fragments   = fragments;
+        this.symbols     = symbols;
         this.dateFormat  = dateFormat;
         this.unitFormat  = unitFormat;
         this.errorLocale = errorLocale;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
index 1610727a53..be411c60d7 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/GeodeticObjectParser.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.io.wkt;
 
+import java.net.URI;
 import java.util.Map;
 import java.util.List;
 import java.util.Locale;
@@ -94,6 +95,7 @@ import org.apache.sis.util.iso.Types;
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
  */
+@SuppressWarnings("LocalVariableHidesMemberVariable")       // We hide with 
the same value made final.
 class GeodeticObjectParser extends MathTransformParser implements 
Comparator<CoordinateSystemAxis> {
     /*
      * Force class initialization of `AxisDirections` in order to have
@@ -182,7 +184,7 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
     public GeodeticObjectParser(final Map<String,?> defaultProperties,
             final ObjectFactory factories, final MathTransformFactory 
mtFactory)
     {
-        super(Symbols.getDefault(), Collections.emptyMap(), null, null, null,
+        super(null, Collections.emptyMap(), Symbols.getDefault(), null, null, 
null,
                 new ReferencingFactoryContainer(defaultProperties,
                         (CRSFactory)   factories,
                         (CSFactory)    factories,
@@ -198,8 +200,9 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
      * Constructs a parser for the specified set of symbols using the 
specified set of factories.
      * This constructor is for {@link WKTFormat} usage only.
      *
-     * @param  symbols       the set of symbols to use.
+     * @param  sourceFile    URI to declare as the source of the WKT 
definitions, or {@code null} if unknown.
      * @param  fragments     reference to the {@link WKTFormat#fragments} map, 
or an empty map if none.
+     * @param  symbols       the set of symbols to use. Cannot be null.
      * @param  numberFormat  the number format provided by {@link WKTFormat}, 
or {@code null} for a default format.
      * @param  dateFormat    the date format provided by {@link WKTFormat}, or 
{@code null} for a default format.
      * @param  unitFormat    the unit format provided by {@link WKTFormat}, or 
{@code null} for a default format.
@@ -207,12 +210,12 @@ class GeodeticObjectParser extends MathTransformParser 
implements Comparator<Coo
      * @param  errorLocale   the locale for error messages (not for parsing), 
or {@code null} for the system default.
      * @param  factories     on input, the factories to use. On output, the 
factories used. Can be null.
      */
-    GeodeticObjectParser(final Symbols symbols, final Map<String,StoredTree> 
fragments,
+    GeodeticObjectParser(final URI sourceFile, final Map<String,StoredTree> 
fragments, final Symbols symbols,
             final NumberFormat numberFormat, final DateFormat dateFormat, 
final UnitFormat unitFormat,
             final Convention convention, final Transliterator transliterator, 
final Locale errorLocale,
             final ReferencingFactoryContainer factories)
     {
-        super(symbols, fragments, numberFormat, dateFormat, unitFormat, 
factories, errorLocale);
+        super(sourceFile, fragments, symbols, numberFormat, dateFormat, 
unitFormat, factories, errorLocale);
         this.transliterator = transliterator;
         usesCommonUnits = convention.usesCommonUnits;
         ignoreAxes      = convention == Convention.WKT1_IGNORE_AXES;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
index 1afebc6c84..af4f45d5d5 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/MathTransformParser.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.io.wkt;
 
+import java.net.URI;
 import java.util.Map;
 import java.util.Arrays;
 import java.util.Locale;
@@ -40,13 +41,13 @@ import org.opengis.referencing.operation.OperationMethod;
 import org.apache.sis.referencing.util.CoordinateOperations;
 import org.apache.sis.referencing.util.ReferencingFactoryContainer;
 import org.apache.sis.referencing.util.WKTKeywords;
+import org.apache.sis.parameter.DefaultParameterValue;
 import org.apache.sis.util.Numbers;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.internal.Constants;
 import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.measure.UnitFormat;
 import org.apache.sis.measure.Units;
-import org.apache.sis.util.resources.Errors;
-import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
 
 
 /**
@@ -134,28 +135,28 @@ class MathTransformParser extends AbstractParser {
      * @param  mtFactory  the factory to use for creating {@link 
MathTransform} objects.
      */
     public MathTransformParser(final MathTransformFactory mtFactory) {
-        this(Symbols.getDefault(), Map.of(), null, null, null,
+        this(null, Map.of(), Symbols.getDefault(), null, null, null,
                 new ReferencingFactoryContainer(null, null, null, null, null, 
mtFactory), null);
     }
 
     /**
      * Creates a parser using the specified set of symbols and factories.
      *
-     * @param  symbols       the set of symbols to use.
+     * @param  sourceFile    URI to declare as the source of the WKT 
definitions, or {@code null} if unknown.
      * @param  fragments     reference to the {@link WKTFormat#fragments} map, 
or an empty map if none.
+     * @param  symbols       the set of symbols to use. Cannot be null.
      * @param  numberFormat  the number format provided by {@link WKTFormat}, 
or {@code null} for a default format.
      * @param  dateFormat    the date format provided by {@link WKTFormat}, or 
{@code null} for a default format.
      * @param  unitFormat    the unit format provided by {@link WKTFormat}, or 
{@code null} for a default format.
      * @param  factories     the factories to use for creating math transforms 
and geodetic objects.
      * @param  errorLocale   the locale for error messages (not for parsing), 
or {@code null} for the system default.
      */
-    MathTransformParser(final Symbols symbols, final Map<String,StoredTree> 
fragments,
+    MathTransformParser(final URI sourceFile, final Map<String,StoredTree> 
fragments, final Symbols symbols,
             final NumberFormat numberFormat, final DateFormat dateFormat, 
final UnitFormat unitFormat,
             final ReferencingFactoryContainer factories, final Locale 
errorLocale)
     {
-        super(symbols, fragments, numberFormat, dateFormat, unitFormat, 
errorLocale);
+        super(sourceFile, fragments, symbols, numberFormat, dateFormat, 
unitFormat, errorLocale);
         this.factories = factories;
-        ensureNonNull("factories", factories);
     }
 
     /**
@@ -347,7 +348,7 @@ class MathTransformParser extends AbstractParser {
         final Unit<?> defaultSI = (defaultUnit != null) ? 
defaultUnit.getSystemUnit() : null;
         Element param = element;
         try {
-            while ((param = element.pullElement(OPTIONAL, 
WKTKeywords.Parameter)) != null) {
+            while ((param = element.pullElement(OPTIONAL, 
WKTKeywords.Parameter, WKTKeywords.ParameterFile)) != null) {
                 final String name = param.pullString("name");
                 Unit<?> unit = parseUnit(param);
                 param.pullElement(OPTIONAL, ID_KEYWORDS);
@@ -386,6 +387,9 @@ class MathTransformParser extends AbstractParser {
                 } else {
                     parameter.setValue(param.pullString("stringValue"));
                 }
+                if (sourceFile != null && parameter instanceof 
DefaultParameterValue<?>) {
+                    ((DefaultParameterValue<?>) 
parameter).setSourceFile(sourceFile);
+                }
                 param.close(ignoredElements);
             }
         } catch (ParameterNotFoundException e) {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Parser.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Parser.java
index eee7b89b1c..1076ef12cc 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Parser.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/Parser.java
@@ -60,9 +60,9 @@ public interface Parser {
      * For processing warnings in a different way than logging them, one can 
use
      * {@link WKTFormat#parseObject(String)} followed by a call to {@link 
WKTFormat#getWarnings()}.
      *
-     * @param  text  object encoded in Well-Known Text format (version 1 or 2).
+     * @param  wkt  object encoded in Well-Known Text format (version 1 or 2).
      * @return the result of parsing the given text.
      * @throws FactoryException if the object creation failed.
      */
-    Object createFromWKT(String text) throws FactoryException;
+    Object createFromWKT(String wkt) throws FactoryException;
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java
index cc7047cc0f..8fcc5b6679 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/io/wkt/WKTFormat.java
@@ -25,10 +25,12 @@ import java.util.HashMap;
 import java.util.TreeMap;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Objects;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.logging.LogRecord;
 import java.util.function.Function;
+import java.net.URI;
 import java.io.IOException;
 import java.text.Format;
 import java.text.NumberFormat;
@@ -119,7 +121,7 @@ import org.opengis.metadata.Identifier;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rémi Eve (IRD)
- * @version 1.4
+ * @version 1.5
  *
  * @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html";>WKT 
2 specification</a>
  * @see <a 
href="http://www.geoapi.org/3.0/javadoc/org/opengis/referencing/doc-files/WKT.html";>Legacy
 WKT 1</a>
@@ -236,6 +238,15 @@ public class WKTFormat extends CompoundFormat<Object> {
      */
     private TreeMap<String,StoredTree> fragments;
 
+    /**
+     * The URI to declare as the source of the WKT definitions, or {@code 
null} if unknown.
+     * This information is not used directly by the parser, but will be stored 
in parameter values
+     * as a hint for resolving relative paths as absolute paths.
+     *
+     * @see #getSourceFile()
+     */
+    private URI sourceFile;
+
     /**
      * {@code true} if the {@link #fragments} map is shared by two or more 
{@code WKTFormat} instances.
      * In such case, the map shall not be modified; instead it must be copied 
before any modification.
@@ -386,12 +397,11 @@ public class WKTFormat extends CompoundFormat<Object> {
     /**
      * Sets the symbols used for parsing and formatting WKT.
      *
-     * @param  symbols  the new set of symbols to use for parsing and 
formatting WKT.
+     * @param  style  the new set of symbols to use for parsing and formatting 
WKT.
      */
-    public void setSymbols(final Symbols symbols) {
-        ArgumentChecks.ensureNonNull("symbols", symbols);
-        if (!symbols.equals(this.symbols)) {
-            this.symbols = symbols.immutable();
+    public void setSymbols(final Symbols style) {
+        if (!style.equals(symbols)) {               // Intentional 
NullPointerException.
+            symbols = style.immutable();
             formatter = null;
             parser = null;
         }
@@ -428,13 +438,13 @@ public class WKTFormat extends CompoundFormat<Object> {
      * then the default mapper is {@link Transliterator#DEFAULT} except for 
WKT formatted according
      * the {@linkplain Convention#INTERNAL internal convention}.</p>
      *
-     * @param  transliterator  the new mapper to use, or {@code null} for 
restoring the default value.
+     * @param  map  the new mapper to use, or {@code null} for restoring the 
default value.
      *
      * @since 0.6
      */
-    public void setTransliterator(final Transliterator transliterator) {
-        if (this.transliterator != transliterator) {
-            this.transliterator = transliterator;
+    public void setTransliterator(final Transliterator map) {
+        if (transliterator != map) {
+            transliterator = map;
             updateFormatter(formatter);
             parser = null;
         }
@@ -452,11 +462,10 @@ public class WKTFormat extends CompoundFormat<Object> {
     /**
      * Sets whether WKT keywords should be written with upper cases or camel 
cases.
      *
-     * @param  keywordCase  the case to use for formatting keywords.
+     * @param  style  the case to use for formatting keywords.
      */
-    public void setKeywordCase(final KeywordCase keywordCase) {
-        ArgumentChecks.ensureNonNull("keywordCase", keywordCase);
-        this.keywordCase = keywordCase;
+    public void setKeywordCase(final KeywordCase style) {
+        keywordCase = Objects.requireNonNull(style);
         updateFormatter(formatter);
     }
 
@@ -474,13 +483,12 @@ public class WKTFormat extends CompoundFormat<Object> {
     /**
      * Sets whether to use short or long WKT keywords.
      *
-     * @param  keywordStyle  the style to use for formatting keywords.
+     * @param  style  the style to use for formatting keywords.
      *
      * @since 0.6
      */
-    public void setKeywordStyle(final KeywordStyle keywordStyle) {
-        ArgumentChecks.ensureNonNull("keywordStyle", keywordStyle);
-        this.keywordStyle = keywordStyle;
+    public void setKeywordStyle(final KeywordStyle style) {
+        keywordStyle = Objects.requireNonNull(style);
         updateFormatter(formatter);
     }
 
@@ -505,13 +513,13 @@ public class WKTFormat extends CompoundFormat<Object> {
      * method tries to highlight most of the elements that are relevant to
      * {@link org.apache.sis.util.Utilities#equalsIgnoreMetadata(Object, 
Object)}.</p>
      *
-     * @param  colors  the colors for syntax coloring, or {@code null} if none.
+     * @param  emphasis  the colors for syntax coloring, or {@code null} if 
none.
      */
-    public void setColors(Colors colors) {
-        if (colors != null) {
-            colors = colors.immutable();
+    public void setColors(Colors emphasis) {
+        if (emphasis != null) {
+            emphasis = emphasis.immutable();
         }
-        this.colors = colors;
+        colors = emphasis;
         updateFormatter(formatter);
     }
 
@@ -528,12 +536,11 @@ public class WKTFormat extends CompoundFormat<Object> {
     /**
      * Sets the convention for parsing and formatting WKT elements.
      *
-     * @param  convention  the new convention to use for parsing and 
formatting WKT elements.
+     * @param  variant  the new convention to use for parsing and formatting 
WKT elements.
      */
-    public void setConvention(final Convention convention) {
-        ArgumentChecks.ensureNonNull("convention", convention);
-        if (this.convention != convention) {
-            this.convention = convention;
+    public void setConvention(final Convention variant) {
+        if (convention != variant) {
+            convention = Objects.requireNonNull(variant);
             updateFormatter(formatter);
             parser = null;
         }
@@ -625,13 +632,13 @@ public class WKTFormat extends CompoundFormat<Object> {
      * Sets a new indentation to be used for formatting objects.
      * The {@value #SINGLE_LINE} value means that the whole WKT is to be 
formatted on a single line.
      *
-     * @param  indentation  the new indentation to use.
+     * @param  numSpaces  the new indentation to use in number of spaces.
      *
      * @see org.apache.sis.setup.OptionKey#INDENTATION
      */
-    public void setIndentation(final int indentation) {
-        ArgumentChecks.ensureBetween("indentation", SINGLE_LINE, 
Byte.MAX_VALUE, indentation);
-        this.indentation = (byte) indentation;
+    public void setIndentation(final int numSpaces) {
+        ArgumentChecks.ensureBetween("indentation", SINGLE_LINE, 
Byte.MAX_VALUE, numSpaces);
+        indentation = (byte) numSpaces;
         updateFormatter(formatter);
     }
 
@@ -658,7 +665,7 @@ public class WKTFormat extends CompoundFormat<Object> {
      * @since 1.0
      */
     public void setMaximumListElements(final int limit) {
-        ArgumentChecks.ensureStrictlyPositive("limit", limit);
+        ArgumentChecks.ensureStrictlyPositive("listSizeLimit", limit);
         listSizeLimit = limit;
         updateFormatter(formatter);
     }
@@ -668,7 +675,7 @@ public class WKTFormat extends CompoundFormat<Object> {
      * explicit {@code ID[…]} or {@code AUTHORITY[…]} element. The main use 
case is for implementing
      * a {@link org.opengis.referencing.crs.CRSAuthorityFactory} backed by 
definitions in WKT format.
      *
-     * <p>Note that this identifier apply to all objects to be created, which 
is generally not desirable.
+     * <p>Note that this identifier applies to all objects to be created, 
which is generally not desirable.
      * Callers should invoke {@code setDefaultIdentifier(null)} in a {@code 
finally} block.</p>
      *
      * <p>This is not a publicly committed API. If we want to make this 
functionality public in a future
@@ -747,9 +754,47 @@ public class WKTFormat extends CompoundFormat<Object> {
         }
     }
 
+    /**
+     * Returns the URI to declare as the source of the WKT definitions. This 
value is not used directly by the parser,
+     * but stored as a hint for allowing users to interpret the {@code 
PARAMETERFILE[…]} value as a file relative to
+     * the file containing the WKT. The default value is {@code null}.
+     *
+     * @return the URI to declare as the source of the WKT definitions, or 
{@code null} if none.
+     *
+     * @see #setSourceFile(URI)
+     * @see org.apache.sis.parameter.DefaultParameterValue#getSourceFile()
+     * @see org.apache.sis.xml.MarshalContext#getDocumentURI()
+     * @see URI#resolve(URI)
+     *
+     * @since 1.5
+     */
+    public URI getSourceFile() {
+        return sourceFile;
+    }
+
+    /**
+     * Sets the URI to declare as the source of the WKT definitions. This 
information will be stored in
+     * {@link org.apache.sis.parameter.DefaultParameterValue#getSourceFile()} 
at WKT parsing time as a
+     * hint for resolving relative paths as absolute paths. This value has no 
effect at formatting time.
+     *
+     * @param  document  URI to the file that contains the WKT definitions to 
parse, or {@code null} if none.
+     *
+     * @see #getSourceFile()
+     * @see org.apache.sis.parameter.DefaultParameterValue#setSourceFile(URI)
+     * @see URI#resolve(URI)
+     *
+     * @since 1.5
+     */
+    public void setSourceFile(final URI document) {
+        if (!Objects.equals(sourceFile, document)) {
+            sourceFile = document;
+            parser = null;
+        }
+    }
+
     /**
      * Returns the type of objects formatted by this class. This method has to 
return {@code Object.class}
-     * since it is the only common parent to all object types accepted by this 
formatter.
+     * because it is the only common parent to all object types accepted by 
this formatter.
      *
      * @return {@code Object.class}
      */
@@ -947,14 +992,12 @@ public class WKTFormat extends CompoundFormat<Object> {
      * @param  modifiable  whether the caller intents to modify the {@link 
#fragments} map.
      */
     private AbstractParser parser(final boolean modifiable) {
-        @SuppressWarnings("LocalVariableHidesMemberVariable")
-        AbstractParser parser = this.parser;
         /*
          * `parser` is always null on a fresh clone. However, the `fragments`
          * map may need to be cloned if the caller intents to modify it.
          */
         if (parser == null || (isCloned & modifiable)) {
-            this.parser = parser = new Parser(symbols, fragments(modifiable),
+            parser = new Parser(sourceFile, fragments(modifiable), symbols,
                     (NumberFormat) getFormat(Number.class),
                     (DateFormat)   getFormat(Date.class),
                     (UnitFormat)   getFormat(Unit.class),
@@ -973,12 +1016,12 @@ public class WKTFormat extends CompoundFormat<Object> {
      */
     private final class Parser extends GeodeticObjectParser implements 
Function<Object,Object> {
         /** Creates a new parser. */
-        Parser(final Symbols symbols, final Map<String,StoredTree> fragments,
-                final NumberFormat numberFormat, final DateFormat dateFormat, 
final UnitFormat unitFormat,
-                final Convention convention, final Transliterator 
transliterator, final Locale errorLocale,
-                final ReferencingFactoryContainer factories)
+        Parser(final URI sourceFile, final Map<String,StoredTree> fragments, 
final Symbols symbols,
+               final NumberFormat numberFormat, final DateFormat dateFormat, 
final UnitFormat unitFormat,
+               final Convention convention, final Transliterator 
transliterator, final Locale errorLocale,
+               final ReferencingFactoryContainer factories)
         {
-            super(symbols, fragments, numberFormat, dateFormat, unitFormat, 
convention, transliterator, errorLocale, factories);
+            super(sourceFile, fragments, symbols, numberFormat, dateFormat, 
unitFormat, convention, transliterator, errorLocale, factories);
         }
 
         /** Returns the source class and method to declare in log records. */
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/AbstractParameterDescriptor.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/AbstractParameterDescriptor.java
index 8d8a015c42..9f89555788 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/AbstractParameterDescriptor.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/AbstractParameterDescriptor.java
@@ -52,8 +52,7 @@ import static org.apache.sis.util.Utilities.deepEquals;
  *     <th class="sep">WPS</th>
  *     <th class="sep">ISO 19115</th>
  *     <th class="sep">Remarks</th>
- *   </tr>
- *   <tr>
+ *   </tr><tr>
  *     <td>{@link #getName() getName()}</td>
  *     <td class="sep">{@code name}</td>
  *     <td class="sep">{@code Identifier}</td>
@@ -67,22 +66,19 @@ import static org.apache.sis.util.Utilities.deepEquals;
  *     <td class="sep">{@code Abstract}</td>
  *     <td class="sep">{@code description}</td>
  *     <td class="sep">Also known as “definition”.</td>
- *   </tr>
- *   <tr>
+ *   </tr><tr>
  *     <td>{@link #getDirection()}</td>
  *     <td class="sep"></td>
  *     <td class="sep"></td>
  *     <td class="sep">{@code direction}</td>
  *     <td class="sep">Tells if the parameter is a WPS {@code Input} or {@code 
Output} structure.</td>
- *   </tr>
- *   <tr>
+ *   </tr><tr>
  *     <td>{@link #getMinimumOccurs()}</td>
  *     <td class="sep">{@code minimumOccurs}</td>
  *     <td class="sep">{@code MinOccurs}</td>
  *     <td class="sep">{@code optionality}</td>
  *     <td class="sep">{@code optionality   = (minimumOccurs > 0)}</td>
- *   </tr>
- *   <tr>
+ *   </tr><tr>
  *     <td>{@link #getMaximumOccurs()}</td>
  *     <td class="sep">{@code maximumOccurs}</td>
  *     <td class="sep">{@code MaxOccurs}</td>
@@ -171,6 +167,7 @@ public abstract class AbstractParameterDescriptor extends 
AbstractIdentifiedObje
      * @param maximumOccurs  the {@linkplain #getMaximumOccurs() maximum 
number of times} that values
      *                       for this parameter group are required, or {@link 
Integer#MAX_VALUE} if no restriction.
      */
+    @SuppressWarnings("this-escape")
     protected AbstractParameterDescriptor(final Map<String,?> properties,
             final int minimumOccurs, final int maximumOccurs)
     {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
index c0d020d969..17ae032cbb 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptor.java
@@ -137,28 +137,23 @@ public class DefaultParameterDescriptor<T> extends 
AbstractParameterDescriptor i
      *     <th>Property name</th>
      *     <th>Value type</th>
      *     <th>Returned by</th>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
      *     <td>{@link org.opengis.metadata.Identifier} or {@link String}</td>
      *     <td>{@link #getName()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td>
      *     <td>{@link org.opengis.util.GenericName} or {@link CharSequence} 
(optionally as array)</td>
      *     <td>{@link #getAlias()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td>
      *     <td>{@link org.opengis.metadata.Identifier} (optionally as 
array)</td>
      *     <td>{@link #getIdentifiers()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#DESCRIPTION_KEY}</td>
      *     <td>{@link org.opengis.util.InternationalString} or {@link 
String}</td>
      *     <td>{@link #getDescription()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.opengis.referencing.IdentifiedObject#REMARKS_KEY}</td>
      *     <td>{@link org.opengis.util.InternationalString} or {@link 
String}</td>
      *     <td>{@link #getRemarks()}</td>
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptorGroup.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptorGroup.java
index aa17f0fcdf..472656654e 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptorGroup.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/DefaultParameterDescriptorGroup.java
@@ -122,28 +122,23 @@ public class DefaultParameterDescriptorGroup extends 
AbstractParameterDescriptor
      *     <th>Property name</th>
      *     <th>Value type</th>
      *     <th>Returned by</th>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
      *     <td>{@link org.opengis.metadata.Identifier} or {@link String}</td>
      *     <td>{@link #getName()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td>
      *     <td>{@link org.opengis.util.GenericName} or {@link CharSequence} 
(optionally as array)</td>
      *     <td>{@link #getAlias()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td>
      *     <td>{@link org.opengis.metadata.Identifier} (optionally as 
array)</td>
      *     <td>{@link #getIdentifiers()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value org.opengis.metadata.Identifier#DESCRIPTION_KEY}</td>
      *     <td>{@link org.opengis.util.InternationalString} or {@link 
String}</td>
      *     <td>{@link #getDescription()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.opengis.referencing.IdentifiedObject#REMARKS_KEY}</td>
      *     <td>{@link org.opengis.util.InternationalString} or {@link 
String}</td>
      *     <td>{@link #getRemarks()}</td>
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 d9817c7107..7cdc01b585 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
@@ -18,6 +18,7 @@ package org.apache.sis.parameter;
 
 import java.lang.reflect.Array;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.logging.Logger;
 import java.io.Serializable;
 import java.io.File;
@@ -41,37 +42,38 @@ import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.io.wkt.Convention;
 import org.apache.sis.io.wkt.ElementKind;
+import org.apache.sis.xml.bind.Context;
 import org.apache.sis.xml.bind.gml.Measure;
 import org.apache.sis.xml.bind.gml.MeasureList;
+import org.apache.sis.xml.util.ExternalLinkHandler;
+import org.apache.sis.metadata.internal.ImplementationHelper;
 import org.apache.sis.referencing.internal.Resources;
 import org.apache.sis.referencing.util.WKTUtilities;
 import org.apache.sis.referencing.util.WKTKeywords;
-import org.apache.sis.metadata.internal.ImplementationHelper;
-import org.apache.sis.util.Numbers;
+import org.apache.sis.math.DecimalFunctions;
+import org.apache.sis.system.Loggers;
+import org.apache.sis.util.Utilities;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.LenientComparable;
 import org.apache.sis.util.ObjectConverters;
 import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.util.Numbers;
 import org.apache.sis.util.internal.Numerics;
-import org.apache.sis.system.Loggers;
-import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
-import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
-import static org.apache.sis.util.Utilities.deepEquals;
 
 
 /**
- * A single parameter value used by an operation method. {@code 
ParameterValue} instances are elements in
- * a {@linkplain DefaultParameterValueGroup parameter value group}, in a way 
similar to {@code Map.Entry}
- * instances in a {@code java.util.Map}.
- *
- * <p>In the context of coordinate operations, most parameter values are 
numeric and can be obtained by the
+ * A single parameter value used by an operation method. Each {@code 
ParameterValue} is a
+ * (<var>key</var>, <var>value</var>) pair, and an arbitrary number of those 
pairs can be
+ * stored in a a {@linkplain DefaultParameterValueGroup parameter value group}.
+ * In the context of coordinate operations, parameter values are often numeric 
and can be obtained by the
  * {@link #intValue()} or {@link #doubleValue()} methods. But other types of 
parameter values are possible
  * and can be handled by the more generic {@link #getValue()} and {@link 
#setValue(Object)} methods.
  * All {@code xxxValue()} methods in this class are convenience methods 
converting the value from {@code Object}
  * to some commonly used types. Those types are specified in ISO 19111 as an 
union of attributes, listed below with
- * the corresponding getter and setter methods:</p>
+ * the corresponding getter and setter methods:
  *
  * <table class="sis">
  *   <caption>Mapping from ISO attributes to getters and setters</caption>
@@ -96,28 +98,44 @@ import static org.apache.sis.util.Utilities.deepEquals;
  *     Class<T> valueClass = parameter.getDescriptor().getValueClass();
  *     }
  *
+ * <h2>Absolute paths of value files</h2>
+ * Parameters that are too complex for being expressed as an {@code int[]}, 
{@code double[]} or {@code String} type
+ * may be encoded in auxiliary files. It is the case, for example, of gridded 
data such as datum shift grids.
+ * The name of an auxiliary file is given by {@link #valueFile()}, but often 
as a <em>relative</em> path.
+ * The directory where that file is located depends on the operation using the 
parameter.
+ * For example, datum shift grids used by coordinate transformations are 
searched in the
+ * {@code $SIS_DATA/DatumChanges} directory, where {@code $SIS_DATA} is the 
value of the environment variable.
+ * However, the latest approach requires that all potentially used auxiliary 
files are preexisting on the local machine.
+ * This assumption may be applicable for parameters coming from a well-known 
registry such as EPSG, but cannot work
+ * with arbitrary operations where the auxiliary files need to be transferred 
together with the parameter values.
+ * For the latter case, an alternative is to consider the auxiliary files as 
relative to the GML document or WKT file
+ * that provides the parameter values. For allowing users to resolve or 
download auxiliary files in that way,
+ * a {@link #getSourceFile()} method is provided. Operations can then use 
{@link URI#resolve(URI)} for getting the
+ * absolute path of an auxiliary file from the same server or directory than 
the GML or WKT file of parameter values.
+ *
  * <h2>Instantiation</h2>
  * A {@linkplain DefaultParameterDescriptor parameter descriptor} must be 
defined before parameter value can be created.
- * Descriptors are usually predefined by map projection or process providers. 
Given a descriptor, a parameter value can
- * be created by a call to the {@link 
#DefaultParameterValue(ParameterDescriptor)} constructor or by a call to the
- * {@link ParameterDescriptor#createValue()} method. The latter is recommended 
since it allows descriptors to return
- * specialized implementations.
+ * Descriptors are usually predefined (often hard-coded) by map projection or 
process providers. Given a descriptor,
+ * the preferred way to create a parameter value is to invoke the {@link 
ParameterDescriptor#createValue()} method.
+ * It is also possible to invoke the {@linkplain 
#DefaultParameterValue(ParameterDescriptor) constructor} directly,
+ * but the former is recommended because it allows descriptors to return 
specialized implementations.
  *
  * <h2>Implementation note for subclasses</h2>
- * All read and write operations (except constructors, {@link #equals(Object)} 
and {@link #hashCode()})
+ * All read and write operations except constructors, {@link #equals(Object)} 
and {@link #hashCode()},
  * ultimately delegates to the following methods:
  *
  * <ul>
- *   <li>All getter methods will invoke {@link #getValue()} and {@link 
#getUnit()} (if needed),
+ *   <li>The source file property is accessed by {@link #getSourceFile()} and 
{@link #setSourceFile(URI)}.</li>
+ *   <li>All other getter methods will invoke {@link #getValue()} and {@link 
#getUnit()} (if needed),
  *       then perform their processing on the values returned by those 
methods.</li>
- *   <li>All setter methods delegate to the {@link #setValue(Object, Unit)} 
method.</li>
+ *   <li>All other setter methods delegate to the {@link #setValue(Object, 
Unit)} method.</li>
  * </ul>
  *
  * Consequently, the above-cited methods provide single points that subclasses 
can override
  * for modifying the behavior of all getter and setter methods.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @param  <T>  the type of the value stored in this parameter.
  *
@@ -175,6 +193,17 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
     @SuppressWarnings("serial")         // Most SIS implementations are 
serializable.
     protected Unit<?> unit;
 
+    /**
+     * If the parameter value is a relative path, the base URI to use for 
resolving that path.
+     * This field is {@code null} by default, unless this parameter value has 
been read from a
+     * GML document or a WKT file, in which case this field contains the URI 
of that document.
+     * This information allows to interpret {@link #valueFile()} as relative 
to the GML or WKT file.
+     *
+     * @see #getSourceFile()
+     * @see URI#resolve(URI)
+     */
+    private URI sourceFile;
+
     /**
      * Creates a parameter value from the specified descriptor.
      * The value will be initialized to the default value, if any.
@@ -182,7 +211,7 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      * @param  descriptor  the abstract definition of this parameter.
      */
     public DefaultParameterValue(final ParameterDescriptor<T> descriptor) {
-        ensureNonNull("descriptor", descriptor);
+        ArgumentChecks.ensureNonNull("descriptor", descriptor);
         this.descriptor = descriptor;
         this.value      = descriptor.getDefaultValue();
         this.unit       = descriptor.getUnit();
@@ -190,8 +219,8 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
 
     /**
      * Creates a new instance initialized with the values from the specified 
parameter object.
-     * This is a <em>shallow</em> copy constructor, since the value contained 
in the given
-     * object is not cloned.
+     * This is a <em>shallow</em> copy constructor, since the {@linkplain 
#getValue() value}
+     * contained in the given object is not cloned.
      *
      * @param  parameter  the parameter to copy values from.
      *
@@ -199,16 +228,17 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      * @see #unmodifiable(ParameterValue)
      */
     public DefaultParameterValue(final ParameterValue<T> parameter) {
-        ensureNonNull("parameter", parameter);
+        ArgumentChecks.ensureNonNull("parameter", parameter);
         descriptor = parameter.getDescriptor();
         value      = parameter.getValue();
         unit       = parameter.getUnit();
+        if (parameter instanceof DefaultParameterValue<?>) {
+            sourceFile = ((DefaultParameterValue<?>) 
parameter).getSourceFile().orElse(null);
+        }
     }
 
     /**
-     * Returns the definition of this parameter.
-     *
-     * @return the definition of this parameter.
+     * {@return the definition of this parameter}.
      */
     @Override
     @XmlElement(name = "operationParameter", required = true)
@@ -216,6 +246,28 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
         return descriptor;
     }
 
+    /**
+     * {@return the URI of the GML document or WKT file from which this 
parameter value has been read}.
+     * This information allows to interpret {@link #valueFile()} as a path 
relative to the file that defined
+     * this parameter value. For example, the following snippet gets the file, 
then tries to make it absolute:
+     *
+     * {@snippet lang="java" :
+     *     DefaultParameterValue<?> pv = ...;
+     *     URI file = pv.valueFile();
+     *     file = pv.getSourceFile().map((base) -> 
base.resolve(file)).orElse(file);
+     *     }
+     *
+     * @see #setSourceFile(URI)
+     * @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() {
+        return Optional.ofNullable(sourceFile);
+    }
+
     /**
      * Returns the unit of measure of the parameter value.
      * If the parameter value has no unit (for example because it is a {@link 
String} type),
@@ -272,6 +324,7 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      */
     @Override
     public boolean booleanValue() throws IllegalStateException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final T value = getValue();
         if (value instanceof Boolean) {
             return (Boolean) value;
@@ -296,6 +349,7 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      */
     @Override
     public int intValue() throws IllegalStateException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final T value = getValue();
         if (value instanceof Number) {
             final int integer = ((Number) value).intValue();
@@ -323,6 +377,7 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      */
     @Override
     public int[] intValueList() throws IllegalStateException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final T value = getValue();
         if (value instanceof int[]) {
             return ((int[]) value).clone();
@@ -349,6 +404,7 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      */
     @Override
     public double doubleValue() throws IllegalStateException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final T value = getValue();
         if (value instanceof Number) {
             return ((Number) value).doubleValue();
@@ -374,6 +430,7 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      */
     @Override
     public double[] doubleValueList() throws IllegalStateException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final T value = getValue();
         if (value instanceof double[]) {
             return ((double[]) value).clone();
@@ -384,20 +441,20 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
     /**
      * Returns the converter to be used by {@link #doubleValue(Unit)} and 
{@link #doubleValueList(Unit)}.
      */
-    private UnitConverter getConverterTo(final Unit<?> unit) {
+    private UnitConverter getConverterTo(final Unit<?> target) {
         final Unit<?> source = getUnit();
         if (source == null) {
             throw new 
IllegalStateException(Resources.format(Resources.Keys.UnitlessParameter_1, 
Verifier.getDisplayName(descriptor)));
         }
-        ensureNonNull("unit", unit);
+        ArgumentChecks.ensureNonNull("unit", target);
         final short expectedID = Verifier.getUnitMessageID(source);
-        if (Verifier.getUnitMessageID(unit) != expectedID) {
-            throw new IllegalArgumentException(Errors.format(expectedID, 
unit));
+        if (Verifier.getUnitMessageID(target) != expectedID) {
+            throw new IllegalArgumentException(Errors.format(expectedID, 
target));
         }
         try {
-            return source.getConverterToAny(unit);
+            return source.getConverterToAny(target);
         } catch (IncommensurableException e) {
-            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IncompatibleUnits_2, source, 
unit), e);
+            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IncompatibleUnits_2, source, 
target), e);
         }
     }
 
@@ -422,6 +479,7 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      */
     @Override
     public double doubleValue(final Unit<?> unit) throws 
IllegalArgumentException, IllegalStateException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final double value = doubleValue();                 // Invoke first in 
case it throws an exception.
         return getConverterTo(unit).convert(value);
     }
@@ -469,6 +527,7 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      */
     @Override
     public String stringValue() throws IllegalStateException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final T value = getValue();
         if (value instanceof CharSequence) {
             return value.toString();
@@ -481,6 +540,12 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      * The default implementation can convert the following value types:
      * {@link URI}, {@link URL}, {@link Path}, {@link File}.
      *
+     * <h4>Relative paths to absolute paths</h4>
+     * This parameter value is often a path relative to an unspecified 
directory. The base directory
+     * depends on the context. For example, it may be a directory where all 
datum grids are cached.
+     * Sometime, it is convenient to interpret the path as relative to the GML 
document or WKT file
+     * that defined this parameter value. For such resolution, see {@link 
#getSourceFile()}.
+     *
      * @return the reference to a file containing parameter values.
      * @throws InvalidParameterTypeException if the value is not a reference 
to a file or a URI.
      * @throws IllegalStateException if the value is not defined and there is 
no default value.
@@ -490,6 +555,7 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
      */
     @Override
     public URI valueFile() throws IllegalStateException {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final T value = getValue();
         if (value instanceof URI)  return   (URI) value;
         if (value instanceof File) return ((File) value).toURI();
@@ -528,6 +594,23 @@ public class DefaultParameterValue<T> extends 
FormattableObject implements Param
         return isFile(newValue);
     }
 
+    /**
+     * Sets the URI of the GML document or WKT file from which this parameter 
value has been read.
+     * The given URI is a hint to be returned by {@link #getSourceFile()} for 
allowing callers to
+     * {@linkplain URI#resolve(URI) resolve} relative {@linkplain #valueFile() 
value files}.
+     *
+     * @param document  URI of the document from which this parameter value 
has been read, or {@code null} if none.
+     *
+     * @see #getSourceFile()
+     * @see org.apache.sis.io.wkt.WKTFormat#setSourceFile(URI)
+     * @see URI#resolve(URI)
+     *
+     * @see 1.5
+     */
+    public void setSourceFile(final URI document) {
+        sourceFile = document;
+    }
+
     /**
      * Returns the exception to throw when an incompatible method is invoked 
for the value type.
      */
@@ -723,8 +806,8 @@ convert:            if (componentType != null) {
         setValue(n, unit);
         /*
          * Above code used `unit` instead of `getUnit()` despite class Javadoc 
claim because units are not expected
-         * to be involved in this method. We access this field only as a 
matter of principle, for making sure that no
-         * property other than the value is altered by this method call.
+         * to be involved in this method. We access this field only as a 
matter of principle, for making sure that
+         * no property other than the value is altered by this method call.
          */
     }
 
@@ -841,15 +924,16 @@ convert:            if (componentType != null) {
             if (mode == ComparisonMode.STRICT) {
                 if (getClass() == object.getClass()) {
                     final DefaultParameterValue<?> that = 
(DefaultParameterValue<?>) object;
-                    return Objects.equals(descriptor, that.descriptor) &&
-                           Objects.deepEquals(value,  that.value) &&
-                           Objects.equals(unit,       that.unit);
+                    return Objects.equals    (descriptor, that.descriptor) &&
+                           Objects.deepEquals(sourceFile, that.sourceFile) &&
+                           Objects.deepEquals(value,      that.value) &&
+                           Objects.equals    (unit,       that.unit);
                 }
             } else if (object instanceof ParameterValue<?>) {
                 final ParameterValue<?> that = (ParameterValue<?>) object;
-                return deepEquals(getDescriptor(), that.getDescriptor(), mode) 
&&
-                       deepEquals(getValue(),      that.getValue(),      mode) 
&&
-                       deepEquals(getUnit(),       that.getUnit(),       mode);
+                return Utilities.deepEquals(getDescriptor(), 
that.getDescriptor(), mode) &&
+                       Utilities.deepEquals(getValue(),      that.getValue(),  
    mode) &&
+                       Utilities.deepEquals(getUnit(),       that.getUnit(),   
    mode);
             }
         }
         return false;
@@ -1162,6 +1246,7 @@ convert:            if (componentType != null) {
         @XmlElement(name = "valueList",         type = MeasureList.class)
     })
     private Object getXmlValue() {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final Object value = getValue();                        // Give to 
user a chance to override.
         if (value != null) {
             if (value instanceof Number) {
@@ -1196,6 +1281,10 @@ convert:            if (componentType != null) {
      */
     @SuppressWarnings("unchecked")
     private void setXmlValue(Object xmlValue) {
+        ExternalLinkHandler linkHandler = 
Context.linkHandler(Context.current());
+        if (linkHandler != null) {
+            sourceFile = linkHandler.getURI();
+        }
         if (value == null && unit == null) {
             if (xmlValue instanceof Measure) {
                 final Measure measure = (Measure) xmlValue;
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/UnmodifiableParameterValue.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/UnmodifiableParameterValue.java
index 8939067bee..fb22b0c6eb 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/UnmodifiableParameterValue.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/UnmodifiableParameterValue.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.parameter;
 
+import java.net.URI;
 import jakarta.xml.bind.annotation.XmlTransient;
 import javax.measure.Unit;
 import org.opengis.parameter.ParameterValue;
@@ -92,6 +93,7 @@ final class UnmodifiableParameterValue<T> extends 
DefaultParameterValue<T> {
      */
     @Override
     public T getValue() {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         T value = super.getValue();
         if (value instanceof Cloneable) {
             final Class<T> type = getDescriptor().getValueClass();      // May 
be null after GML unmarshalling.
@@ -112,6 +114,14 @@ final class UnmodifiableParameterValue<T> extends 
DefaultParameterValue<T> {
         throw new 
UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, 
getClass()));
     }
 
+    /**
+     * Do not allow modification of the parameter value.
+     */
+    @Override
+    public void setSourceFile(final URI source) {
+        throw new 
UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, 
getClass()));
+    }
+
     /**
      * Returns a modifiable copy of this parameter.
      */
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/package-info.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/package-info.java
index 4b3a2e0910..1299b3c4eb 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/package-info.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/package-info.java
@@ -85,7 +85,7 @@
  * if the given value is not assignable to the expected class or is not inside 
the value domain.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.4
  */
 @XmlSchema(elementFormDefault= XmlNsForm.QUALIFIED, namespace = 
Namespaces.GML, xmlns = {
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
index ca0cba5a49..cc2600eaa3 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
@@ -306,7 +306,7 @@ public final class CRS extends Static {
      * Applications which need to parse a large amount of WKT strings should 
consider to use
      * the {@link org.apache.sis.io.wkt.WKTFormat} class instead of this 
method.
      *
-     * @param  text  coordinate system encoded in Well-Known Text format 
(version 1 or 2).
+     * @param  wkt  coordinate system encoded in Well-Known Text format 
(version 1 or 2).
      * @return the parsed Coordinate Reference System.
      * @throws FactoryException if the given WKT cannot be parsed.
      *
@@ -317,9 +317,8 @@ public final class CRS extends Static {
      *
      * @since 0.6
      */
-    public static CoordinateReferenceSystem fromWKT(final String text) throws 
FactoryException {
-        ArgumentChecks.ensureNonNull("text", text);
-        final CoordinateReferenceSystem crs = 
GeodeticObjectFactory.provider().createFromWKT(text);
+    public static CoordinateReferenceSystem fromWKT(final String wkt) throws 
FactoryException {
+        final CoordinateReferenceSystem crs = 
GeodeticObjectFactory.provider().createFromWKT(wkt);
         DefinitionVerifier.withAuthority(crs, Loggers.WKT, CRS.class, 
"fromWKT");
         return crs;
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
index 95c4401371..ca55b08f28 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
@@ -1661,7 +1661,7 @@ public class GeodeticObjectFactory extends 
AbstractFactory implements CRSFactory
      * Applications which need to parse a large amount of WKT strings should 
consider to use
      * the {@link org.apache.sis.io.wkt.WKTFormat} class instead of this 
method.
      *
-     * @param  text  coordinate system encoded in Well-Known Text format 
(version 1 or 2).
+     * @param  wkt  coordinate system encoded in Well-Known Text format 
(version 1 or 2).
      * @throws FactoryException if the object creation failed.
      *
      * @see org.apache.sis.io.wkt
@@ -1670,8 +1670,8 @@ public class GeodeticObjectFactory extends 
AbstractFactory implements CRSFactory
      * @see <a 
href="http://www.geoapi.org/3.0/javadoc/org/opengis/referencing/doc-files/WKT.html";>Legacy
 WKT 1</a>
      */
     @Override
-    public CoordinateReferenceSystem createFromWKT(final String text) throws 
FactoryException {
-        ArgumentChecks.ensureNonEmpty("text", text);
+    public CoordinateReferenceSystem createFromWKT(final String wkt) throws 
FactoryException {
+        ArgumentChecks.ensureNonEmpty("wkt", wkt);
         Parser p = parser.getAndSet(null);
         if (p == null) try {
             Constructor<? extends Parser> c = parserConstructor;
@@ -1687,7 +1687,7 @@ public class GeodeticObjectFactory extends 
AbstractFactory implements CRSFactory
         }
         final Object object;
         try {
-            object = p.createFromWKT(text);
+            object = p.createFromWKT(wkt);
         } catch (FactoryException e) {
             /*
              * In the case of map projection, the parsing may fail because a 
projection parameter is not known to SIS.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
index e634c53788..6360e9bdd4 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
@@ -1659,15 +1659,15 @@ public class DefaultMathTransformFactory extends 
AbstractFactory implements Math
      * inverse matrix but those information are lost at WKT formatting time. A 
similar "hidden" information
      * lost may also happen with {@link WraparoundTransform}, also making that 
transform non-invertible.</p>
      *
-     * @param  text  math transform encoded in Well-Known Text format.
+     * @param  wkt  math transform encoded in Well-Known Text format.
      * @return the math transform (never {@code null}).
      * @throws FactoryException if the Well-Known Text cannot be parsed,
      *         or if the math transform creation failed from some other reason.
      */
     @Override
-    public MathTransform createFromWKT(final String text) throws 
FactoryException {
+    public MathTransform createFromWKT(final String wkt) throws 
FactoryException {
         lastMethod.remove();
-        ArgumentChecks.ensureNonEmpty("text", text);
+        ArgumentChecks.ensureNonEmpty("wkt", wkt);
         Parser p = parser.getAndSet(null);
         if (p == null) try {
             Constructor<? extends Parser> c = parserConstructor;
@@ -1687,7 +1687,7 @@ public class DefaultMathTransformFactory extends 
AbstractFactory implements Math
          */
         final Object object;
         try {
-            object = p.createFromWKT(text);
+            object = p.createFromWKT(wkt);
         } catch (FactoryException e) {
             /*
              * The parsing may fail because a operation parameter is not known 
to SIS. If this happen, replace
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
index b807ffad0f..c8ddc5db8c 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/ElementTest.java
@@ -46,8 +46,8 @@ public final class ElementTest extends TestCase {
     /**
      * A dummy parser to be given to the {@link Element} constructor.
      */
-    private final AbstractParser parser = new 
AbstractParser(Symbols.SQUARE_BRACKETS, new HashMap<>(2),
-            null, null, null, Locale.ENGLISH)
+    private final AbstractParser parser = new AbstractParser(
+            null, new HashMap<>(2), Symbols.SQUARE_BRACKETS, null, null, null, 
Locale.ENGLISH)
     {
         @Override String getPublicFacade() {
             throw new UnsupportedOperationException();
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
index 8ff779723e..32e34d9968 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
@@ -92,8 +92,8 @@ public final class GeodeticObjectParserTest extends TestCase {
      * Instantiates the parser to test.
      */
     private void newParser(final Convention convention) {
-        parser = new GeodeticObjectParser(Symbols.getDefault(), Map.of(),
-                null, null, null, convention, Transliterator.DEFAULT, null, 
new ReferencingFactoryContainer());
+        parser = new GeodeticObjectParser(null, Map.of(), 
Symbols.getDefault(), null, null, null,
+                        convention, Transliterator.DEFAULT, null, new 
ReferencingFactoryContainer());
         assertEquals(GeodeticObjectFactory.class.getCanonicalName(), 
parser.getPublicFacade());
     }
 
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
index 3249a57314..13449a229e 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/PRJDataStore.java
@@ -144,7 +144,7 @@ public abstract class PRJDataStore extends URIDataStore {
      * @throws DataStoreException if an error occurred while reading the file.
      */
     protected final void readPRJ() throws DataStoreException {
-        Exception cause = null;
+        Exception cause = null, suppressed = null;
         try {
             final AuxiliaryContent content = readAuxiliaryFile(PRJ);
             if (content == null) {
@@ -154,6 +154,11 @@ public abstract class PRJDataStore extends URIDataStore {
             final String wkt = content.toString();
             final StoreFormat format = new StoreFormat(locale, timezone, null, 
listeners);
             format.setConvention(Convention.WKT1_COMMON_UNITS);         // 
Ignored if the format is WKT 2.
+            try {
+                format.setSourceFile(content.getURI());
+            } catch (URISyntaxException e) {
+                suppressed = e;
+            }
             final ParsePosition pos = new ParsePosition(0);
             crs = (CoordinateReferenceSystem) format.parse(wkt, pos);
             if (crs != null) {
@@ -171,7 +176,9 @@ public abstract class PRJDataStore extends URIDataStore {
         } catch (IOException | ParseException | ClassCastException e) {
             cause = e;
         }
-        throw new DataStoreReferencingException(cannotReadAuxiliaryFile(PRJ), 
cause);
+        final var e = new 
DataStoreReferencingException(cannotReadAuxiliaryFile(PRJ), cause);
+        if (suppressed != null) e.addSuppressed(suppressed);
+        throw e;
     }
 
     /**
@@ -273,6 +280,16 @@ public abstract class PRJDataStore extends URIDataStore {
             return IOUtilities.filename(source);
         }
 
+        /**
+         * Returns the source as an URI if possible.
+         *
+         * @return the source as an URI, or {@code null} if none.
+         * @throws URISyntaxException if the URI cannot be parsed.
+         */
+        public URI getURI() throws URISyntaxException {
+            return IOUtilities.toURI(source);
+        }
+
         /**
          * Returns the number of valid characters in this sequence.
          */

Reply via email to