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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 3a5fcfaa4a Generalize the update of properties of operations to all 
kinds of `AbstractOperation` instead of only links. This is a generalization of 
the previous commit ("recreate the result types of the links if they changed"). 
The intent is to the CRS characteristic updated not only for links, but also 
for the envelope operation.
3a5fcfaa4a is described below

commit 3a5fcfaa4a32f78737e5b4ded29aff5827b4828d
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat May 24 18:46:52 2025 +0200

    Generalize the update of properties of operations to all kinds of 
`AbstractOperation` instead of only links.
    This is a generalization of the previous commit ("recreate the result types 
of the links if they changed").
    The intent is to the CRS characteristic updated not only for links, but 
also for the envelope operation.
---
 .../apache/sis/feature/AbstractIdentifiedType.java |  90 +++++++---
 .../org/apache/sis/feature/AbstractOperation.java  |  65 ++++---
 .../apache/sis/feature/DefaultAssociationRole.java |  15 +-
 .../apache/sis/feature/DefaultAttributeType.java   |  15 +-
 .../org/apache/sis/feature/EnvelopeOperation.java  | 196 ++++++++++++++-------
 .../org/apache/sis/feature/FeatureOperations.java  |  18 +-
 .../main/org/apache/sis/feature/Features.java      |   4 +-
 .../sis/feature/GroupAsPolylineOperation.java      |  42 ++++-
 .../main/org/apache/sis/feature/LinkOperation.java |  16 ++
 .../apache/sis/feature/StringJoinOperation.java    |  80 +++++++--
 .../sis/feature/builder/OperationWrapper.java      |  37 ++--
 .../org/apache/sis/feature/internal/Resources.java |   5 +
 .../sis/feature/internal/Resources.properties      |   1 +
 .../sis/feature/internal/Resources_fr.properties   |   1 +
 .../feature/privy/FeatureProjectionBuilder.java    |  49 +++---
 .../main/org/apache/sis/storage/FeatureQuery.java  |  13 +-
 .../main/org/apache/sis/storage/FeatureSet.java    |   1 -
 .../main/org/apache/sis/storage/FeatureSubset.java |   4 +-
 .../sis/storage/UnsupportedQueryException.java     |  12 ++
 .../org/apache/sis/storage/FeatureQueryTest.java   |   2 +-
 .../main/org/apache/sis/pending/jdk/JDK21.java     |  24 ++-
 .../org/apache/sis/util/privy/CollectionsExt.java  |   8 +-
 22 files changed, 482 insertions(+), 216 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractIdentifiedType.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractIdentifiedType.java
index d378c95ada..f2beb1c0e2 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractIdentifiedType.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractIdentifiedType.java
@@ -27,6 +27,7 @@ import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.apache.sis.system.Modules;
 import org.apache.sis.util.Deprecable;
+import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.iso.DefaultNameFactory;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.util.resources.Errors;
@@ -98,6 +99,18 @@ public class AbstractIdentifiedType implements 
IdentifiedType, Deprecable, Seria
      */
     public static final String DEPRECATED_KEY = "deprecated";
 
+    /**
+     * Optional key which can be given to the constructor for inheriting 
values from an existing identified type.
+     * If a value exists, then any property that is not defined by one of the 
above-cited keys will inherit its
+     * value from the given {@link IdentifiedType}.
+     *
+     * <p>This property is useful when creating a new property derived from an 
existing property.
+     * An example of such derivation is {@link 
AbstractOperation#updateDependencies(Map)}.</p>
+     *
+     * @since 1.5
+     */
+    public static final String INHERIT_FROM_KEY = "inheritFrom";
+
     /**
      * The name of this type.
      *
@@ -146,7 +159,8 @@ public class AbstractIdentifiedType implements 
IdentifiedType, Deprecable, Seria
 
     /**
      * Constructs a type from the given properties. Keys are strings from the 
table below.
-     * The map given in argument shall contain an entry at least for the 
{@value #NAME_KEY}.
+     * The map given in argument shall contain an entry at least for the 
{@value #NAME_KEY} key,
+     * unless a fallback is specified with the {@value #INHERIT_FROM_KEY} key.
      * Other entries listed in the table below are optional.
      *
      * <table class="sis">
@@ -155,32 +169,31 @@ public class AbstractIdentifiedType implements 
IdentifiedType, Deprecable, Seria
      *     <th>Map key</th>
      *     <th>Value type</th>
      *     <th>Returned by</th>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value #NAME_KEY}</td>
      *     <td>{@link GenericName} or {@link String}</td>
      *     <td>{@link #getName()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value #DEFINITION_KEY}</td>
      *     <td>{@link InternationalString} or {@link String}</td>
      *     <td>{@link #getDefinition()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value #DESIGNATION_KEY}</td>
      *     <td>{@link InternationalString} or {@link String}</td>
      *     <td>{@link #getDesignation()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value #DESCRIPTION_KEY}</td>
      *     <td>{@link InternationalString} or {@link String}</td>
      *     <td>{@link #getDescription()}</td>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value #DEPRECATED_KEY}</td>
      *     <td>{@link Boolean}</td>
      *     <td>{@link #isDeprecated()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
+     *     <td>{@value #INHERIT_FROM_KEY}</td>
+     *     <td>{@link IdentifiedType}</td>
+     *     <td>(various)</td>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.referencing.AbstractIdentifiedObject#LOCALE_KEY}</td>
      *     <td>{@link Locale}</td>
      *     <td>(none)</td>
@@ -202,11 +215,13 @@ public class AbstractIdentifiedType implements 
IdentifiedType, Deprecable, Seria
      */
     @SuppressWarnings("this-escape")
     protected AbstractIdentifiedType(final Map<String,?> identification) 
throws IllegalArgumentException {
-        // Implicit null value check.
-        Object value = identification.get(NAME_KEY);
+        final IdentifiedType inheritFrom = Containers.property(identification, 
INHERIT_FROM_KEY, IdentifiedType.class);
+        Object value = identification.get(NAME_KEY);    // Implicit null value 
check.
         if (value == null) {
-            throw new 
IllegalArgumentException(Errors.forProperties(identification)
-                    .getString(Errors.Keys.MissingValueForProperty_1, 
NAME_KEY));
+            if (inheritFrom == null || (name = inheritFrom.getName()) == null) 
{
+                throw new 
IllegalArgumentException(Errors.forProperties(identification)
+                        .getString(Errors.Keys.MissingValueForProperty_1, 
NAME_KEY));
+            }
         } else if (value instanceof String) {
             name = createName(DefaultNameFactory.provider(), (String) value);
         } else if (value instanceof GenericName) {
@@ -214,12 +229,12 @@ public class AbstractIdentifiedType implements 
IdentifiedType, Deprecable, Seria
         } else {
             throw illegalPropertyType(identification, NAME_KEY, value);
         }
-        definition  = Types.toInternationalString(identification, 
DEFINITION_KEY);
-        designation = Types.toInternationalString(identification, 
DESIGNATION_KEY);
-        description = Types.toInternationalString(identification, 
DESCRIPTION_KEY);
+        definition  = toInternationalString(identification, DEFINITION_KEY,  
inheritFrom);
+        designation = toInternationalString(identification, DESIGNATION_KEY, 
inheritFrom);
+        description = toInternationalString(identification, DESCRIPTION_KEY, 
inheritFrom);
         value = identification.get(DEPRECATED_KEY);
         if (value == null) {
-            deprecated = false;
+            deprecated = (inheritFrom instanceof Deprecable) ? ((Deprecable) 
inheritFrom).isDeprecated() : false;
         } else if (value instanceof Boolean) {
             deprecated = (Boolean) value;
         } else {
@@ -227,6 +242,29 @@ public class AbstractIdentifiedType implements 
IdentifiedType, Deprecable, Seria
         }
     }
 
+    /**
+     * Returns an international string for the values in the given properties 
map, or {@code null} if none.
+     *
+     * @param  identification  the map from which to get the string values for 
an international string.
+     * @param  prefix          the prefix of keys to use for creating the 
international string.
+     * @param  inheritFrom     the type from which to inherit a value if none 
is specified in the map, or {@code null}.
+     * @return the international string, or {@code null} if the given map is 
null or does not contain values
+     *         associated to keys starting with the given prefix.
+     */
+    private static InternationalString toInternationalString(
+            final Map<String,?> identification, final String prefix, final 
IdentifiedType inheritFrom)
+    {
+        InternationalString i18n = Types.toInternationalString(identification, 
prefix);
+        if (i18n == null && inheritFrom != null) {
+            switch (prefix) {
+                case DEFINITION_KEY:  i18n = inheritFrom.getDefinition(); 
break;
+                case DESIGNATION_KEY: i18n = 
inheritFrom.getDesignation().orElse(null); break;
+                case DESCRIPTION_KEY: i18n = 
inheritFrom.getDescription().orElse(null); break;
+            }
+        }
+        return i18n;
+    }
+
     /**
      * Returns the exception to be thrown when a property is of illegal type.
      */
@@ -237,6 +275,14 @@ public class AbstractIdentifiedType implements 
IdentifiedType, Deprecable, Seria
                 Errors.Keys.IllegalPropertyValueClass_2, key, 
value.getClass()));
     }
 
+    /**
+     * Convenience method for subclasses that create new types derived from 
this type.
+     * The purpose is more to improve readability than to save a few byte 
codes.
+     */
+    final Map<String,?> inherit() {
+        return Map.of(INHERIT_FROM_KEY, this);
+    }
+
     /**
      * Creates a name from the given string. This method is invoked at 
construction time,
      * so it should not use any field in this {@code AbtractIdentifiedObject} 
instance.
@@ -367,7 +413,7 @@ public class AbstractIdentifiedType implements 
IdentifiedType, Deprecable, Seria
     @Override
     public boolean equals(final Object obj) {
         if (obj != null && getClass() == obj.getClass()) {
-            final AbstractIdentifiedType that = (AbstractIdentifiedType) obj;
+            final var that = (AbstractIdentifiedType) obj;
             return Objects.equals(name,        that.name) &&
                    Objects.equals(definition,  that.definition) &&
                    Objects.equals(designation, that.designation) &&
@@ -396,7 +442,7 @@ public class AbstractIdentifiedType implements 
IdentifiedType, Deprecable, Seria
             }
             key = Errors.Keys.EmptyProperty_1;
         }
-        final StringBuilder b = new 
StringBuilder(40).append("Type[“").append(container.getName()).append("”].")
+        final var b = new 
StringBuilder(40).append("Type[“").append(container.getName()).append("”].")
                 .append(argument).append('[').append(index).append("].name");
         throw new IllegalArgumentException(Errors.format(key, b.toString()));
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
index cd7d311557..6c76e4c245 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
@@ -19,7 +19,6 @@ package org.apache.sis.feature;
 import java.util.Map;
 import java.util.Set;
 import java.util.Objects;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.function.BiFunction;
 import java.io.IOException;
@@ -43,6 +42,7 @@ import org.opengis.feature.FeatureOperationException;
 import org.opengis.feature.IdentifiedType;
 import org.opengis.feature.Operation;
 import org.opengis.feature.Property;
+import org.opengis.feature.PropertyType;
 
 
 /**
@@ -60,11 +60,8 @@ import org.opengis.feature.Property;
  * The value is computed, or the operation is executed, by {@link 
#apply(Feature, ParameterValueGroup)}.
  * If the value is modifiable, new value can be set by call to {@link 
Attribute#setValue(Object)}.
  *
- * <div class="warning"><b>Warning:</b> this class is experimental and may 
change after we gained more
- * experience on this aspect of ISO 19109.</div>
- *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.5
  *
  * @see DefaultFeatureType
  *
@@ -95,28 +92,23 @@ public abstract class AbstractOperation extends 
AbstractIdentifiedType implement
      *     <th>Map key</th>
      *     <th>Value type</th>
      *     <th>Returned by</th>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#NAME_KEY}</td>
      *     <td>{@link GenericName} or {@link String}</td>
      *     <td>{@link #getName()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DEFINITION_KEY}</td>
      *     <td>{@link org.opengis.util.InternationalString} or {@link 
String}</td>
      *     <td>{@link #getDefinition()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DESIGNATION_KEY}</td>
      *     <td>{@link org.opengis.util.InternationalString} or {@link 
String}</td>
      *     <td>{@link #getDesignation()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DESCRIPTION_KEY}</td>
      *     <td>{@link org.opengis.util.InternationalString} or {@link 
String}</td>
      *     <td>{@link #getDescription()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DEPRECATED_KEY}</td>
      *     <td>{@link Boolean}</td>
      *     <td>{@link #isDeprecated()}</td>
@@ -138,7 +130,7 @@ public abstract class AbstractOperation extends 
AbstractIdentifiedType implement
      * @param  identification  the map given by user to sub-class constructor.
      */
     final Map<String,Object> resultIdentification(final Map<String,?> 
identification) {
-        final Map<String,Object> properties = new HashMap<>(6);
+        final var properties = new HashMap<String,Object>(6);
         for (final Map.Entry<String,?> entry : identification.entrySet()) {
             final String key = entry.getKey();
             if (key != null && key.startsWith(RESULT_PREFIX)) {
@@ -207,18 +199,47 @@ public abstract class AbstractOperation extends 
AbstractIdentifiedType implement
      * other dependencies, the returned set will contain the name of that 
operation but not the names of the
      * dependencies of that operation (unless they are the same that the 
direct dependencies of {@code this}).
      *
-     * <div class="note"><b>Rational:</b>
-     * this information is needed for writing the {@code SELECT} SQL statement 
to send to a database server.
+     * <h4>Purpose</h4>
+     * This information is needed for writing the {@code SELECT} SQL statement 
to send to a database server.
      * The requested columns will typically be all attributes declared in a 
{@code FeatureType}, but also
      * any additional columns needed for the operation while not necessarily 
included in the {@code FeatureType}.
-     * </div>
      *
+     * <h4>Default implementation</h4>
      * The default implementation returns an empty set.
      *
      * @return the names of feature properties needed by this operation for 
performing its task.
      */
     public Set<String> getDependencies() {
-        return Collections.emptySet();
+        return Set.of();
+    }
+
+    /**
+     * Returns the same operation but using different properties as inputs.
+     * The keys in the given map should be values returned by {@link 
#getDependencies()},
+     * and the associated values shall be the properties to use instead of the 
current dependencies.
+     * If any key in the given map is not a member of the {@linkplain 
#getDependencies() dependency set},
+     * then the entry is ignored. Conversely, if any member of the dependency 
set is not contained in the
+     * given map, then the associated dependency is unchanged.
+     *
+     * <h4>Purpose</h4>
+     * This method is needed by {@link 
org.apache.sis.feature.builder.FeatureTypeBuilder} when some properties
+     * are operations inherited from another feature type. Even if the 
dependencies are properties of the same
+     * name, some {@link DefaultAttributeType#characteristics() 
characteristics} may be different.
+     * For example, the <abbr>CRS</abbr> may change as a result of a change of 
<abbr>CRS</abbr>.
+     *
+     * <h4>Default implementation</h4>
+     * The default implementation returns {@code this}.
+     * This is consistent with the default implementation of {@link 
#getDependencies()} returning an empty set.
+     *
+     * @param  dependencies  the new properties to use as operation inputs.
+     * @return the new operation, or {@code this} if unchanged.
+     *
+     * @see #INHERIT_FROM_KEY
+     *
+     * @since 1.5
+     */
+    public Operation updateDependencies(final Map<String, PropertyType> 
dependencies) {
+        return this;
     }
 
     /**
@@ -246,7 +267,7 @@ public abstract class AbstractOperation extends 
AbstractIdentifiedType implement
             return true;
         }
         if (super.equals(obj)) {
-            final AbstractOperation that = (AbstractOperation) obj;
+            final var that = (AbstractOperation) obj;
             return Objects.equals(getParameters(), that.getParameters()) &&
                    Objects.equals(getResult(),     that.getResult());
         }
@@ -261,7 +282,7 @@ public abstract class AbstractOperation extends 
AbstractIdentifiedType implement
      */
     @Override
     public String toString() {
-        final StringBuilder buffer = new 
StringBuilder(40).append(Classes.getShortClassName(this)).append('[');
+        final var buffer = new 
StringBuilder(40).append(Classes.getShortClassName(this)).append('[');
         final GenericName name = getName();
         if (name != null) {
             buffer.append('“');
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
index 3e8061aa23..2dbaf6fe30 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
@@ -96,28 +96,23 @@ public class DefaultAssociationRole extends FieldType 
implements FeatureAssociat
      *     <th>Map key</th>
      *     <th>Value type</th>
      *     <th>Returned by</th>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#NAME_KEY}</td>
      *     <td>{@link GenericName} or {@link String}</td>
      *     <td>{@link #getName()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DEFINITION_KEY}</td>
      *     <td>{@link InternationalString} or {@link String}</td>
      *     <td>{@link #getDefinition()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DESIGNATION_KEY}</td>
      *     <td>{@link InternationalString} or {@link String}</td>
      *     <td>{@link #getDesignation()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DESCRIPTION_KEY}</td>
      *     <td>{@link InternationalString} or {@link String}</td>
      *     <td>{@link #getDescription()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DEPRECATED_KEY}</td>
      *     <td>{@link Boolean}</td>
      *     <td>{@link #isDeprecated()}</td>
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAttributeType.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAttributeType.java
index b5b5c12a55..9d53cb2ba4 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAttributeType.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAttributeType.java
@@ -142,28 +142,23 @@ public class DefaultAttributeType<V> extends FieldType 
implements AttributeType<
      *     <th>Map key</th>
      *     <th>Value type</th>
      *     <th>Returned by</th>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#NAME_KEY}</td>
      *     <td>{@link GenericName} or {@link String}</td>
      *     <td>{@link #getName()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DEFINITION_KEY}</td>
      *     <td>{@link InternationalString} or {@link String}</td>
      *     <td>{@link #getDefinition()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DESIGNATION_KEY}</td>
      *     <td>{@link InternationalString} or {@link String}</td>
      *     <td>{@link #getDesignation()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DESCRIPTION_KEY}</td>
      *     <td>{@link InternationalString} or {@link String}</td>
      *     <td>{@link #getDescription()}</td>
-     *   </tr>
-     *   <tr>
+     *   </tr><tr>
      *     <td>{@value 
org.apache.sis.feature.AbstractIdentifiedType#DEPRECATED_KEY}</td>
      *     <td>{@link Boolean}</td>
      *     <td>{@link #isDeprecated()}</td>
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
index fcf5897599..ca03d43069 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/EnvelopeOperation.java
@@ -21,8 +21,6 @@ import java.util.Set;
 import java.util.Map;
 import java.util.LinkedHashMap;
 import java.util.Objects;
-import java.util.Optional;
-import org.opengis.util.GenericName;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
 import org.opengis.parameter.ParameterDescriptorGroup;
@@ -37,22 +35,25 @@ import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.util.privy.CollectionsExt;
-import org.apache.sis.referencing.CRS;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.pending.jdk.JDK21;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.feature.Attribute;
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureInstantiationException;
 import org.opengis.feature.IdentifiedType;
+import org.opengis.feature.Operation;
 import org.opengis.feature.Property;
 import org.opengis.feature.PropertyType;
 
 
 /**
  * An operation computing the envelope that encompass all geometries found in 
a list of attributes.
- * Geometries can be in different coordinate reference systems; they will be 
transformed to the first
- * non-null CRS in the following choices:
+ * Geometries can be in different coordinate reference systems. They will be 
transformed to the first
+ * non-null <abbr>CRS</abbr> in the following choices:
  *
  * <ol>
  *   <li>the CRS specified at construction time,</li>
@@ -75,7 +76,7 @@ final class EnvelopeOperation extends AbstractOperation {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = 8034615858550405350L;
+    private static final long serialVersionUID = 2435142477482749321L;
 
     /**
      * The parameter descriptor for the "Envelope" operation, which does not 
take any parameter.
@@ -83,7 +84,10 @@ final class EnvelopeOperation extends AbstractOperation {
     private static final ParameterDescriptorGroup EMPTY_PARAMS = 
parameters("Envelope");
 
     /**
-     * The names of all properties containing a geometry object.
+     * The names of all properties containing a geometry object. The 
attributes are the in order specified by the user,
+     * except the default geometry (if any) which is always first. Note that 
the name of the default geometry in this
+     * array is usually <em>not</em> {@value AttributeConvention#GEOMETRY}, 
because this class replaces links by their
+     * targets for avoiding to process the same geometries twice.
      */
     private final String[] attributeNames;
 
@@ -96,6 +100,12 @@ final class EnvelopeOperation extends AbstractOperation {
     @SuppressWarnings("serial")                 // Most SIS implementations 
are serializable.
     final CoordinateReferenceSystem targetCRS;
 
+    /**
+     * Whether {@link #targetCRS} has been explicitly specified by the user.
+     * If {@code false}, then the <abbr>CRS</abbr> has been inherited from the 
geometries.
+     */
+    private final boolean explicitCRS;
+
     /**
      * The coordinate conversions or transformations from the CRS used by the 
geometries to the CRS requested
      * by the user, or {@code null} if there is no operation to apply.  If 
non-null, the length of this array
@@ -117,8 +127,11 @@ final class EnvelopeOperation extends AbstractOperation {
 
     /**
      * The property names as an unmodifiable set, created when first needed.
+     * This is simply {@link #attributeNames} copied in a unmodifiable set.
+     *
+     * @see #getDependencies()
      */
-    private transient Set<String> dependencies;
+    private transient volatile Set<String> dependencies;
 
     /**
      * The type of the result returned by the envelope operation.
@@ -128,77 +141,114 @@ final class EnvelopeOperation extends AbstractOperation {
 
     /**
      * Creates a new operation computing the envelope of features of the given 
type.
+     * The {@link #targetCRS} is set to the first non-null <abbr>CRS</abbr> in 
the following choices:
+     *
+     * <ol>
+     *   <li>the <abbr>CRS</abbr> specified to this constructor,</li>
+     *   <li>the <abbr>CRS</abbr> of the default geometry, or</li>
+     *   <li>the <abbr>CRS</abbr> of the first non-empty geometry.</li>
+     * </ol>
+     *
+     * <h4>Inheritance</h4>
+     * If {@code inheritFrom} is non-null, then the {@code geometryAttributes} 
array must have the same length
+     * as {@code inheritFrom.attributeNames} with elements in the same order. 
Any null element in the given
+     * array will be replaced by the corresponding value of {@code 
inheritFrom}.
      *
      * @param identification      the name and other information to be given 
to this operation.
      * @param targetCRS           the coordinate reference system of envelopes 
to computes, or {@code null}.
      * @param geometryAttributes  the operation or attribute type from which 
to get geometry values.
+     * @param inheritFrom         the existing operation from which to inherit 
attributes, or {@code null}.
      */
-    EnvelopeOperation(final Map<String,?> identification, 
CoordinateReferenceSystem targetCRS,
-            final PropertyType[] geometryAttributes) throws FactoryException
+    EnvelopeOperation(final Map<String,?> identification,
+                      CoordinateReferenceSystem targetCRS,
+                      final PropertyType[] geometryAttributes,
+                      final EnvelopeOperation inheritFrom)
+            throws FactoryException
     {
         super(identification);
-        String defaultGeometry = null;
+        explicitCRS = (targetCRS != null);      // Whether the CRS was 
specified by the user or inferred automatically.
+        boolean characterizedByCRS = false;     // Whether "sis:crs" 
characteristics exist, possibly with null values.
+        String defaultGeometry = null;          // Attribute name of the 
target of the "sis:geometry" property.
+        boolean defaultIsFirst = true;          // Whether the default 
geometry is the first entry in the `names` map.
         /*
-         * Get all property names without duplicated values. If a property is 
a link to an attribute,
-         * then the key will be the name of the referenced attribute instead 
of the operation name.
-         * The intent is to avoid querying the same geometry twice if the 
attribute is also specified
-         * explicitly in the array of properties.
-         *
-         * The map values will be the default Coordinate Reference System, or 
null if none.
+         * Get all property names without duplicated values, including the 
targets of links.
+         * The map values will be the default Coordinate Reference Systems, or 
null if none.
          */
-        boolean characterizedByCRS = false;
-        final Map<String,CoordinateReferenceSystem> names = new 
LinkedHashMap<>(4);
-        for (final IdentifiedType property : geometryAttributes) {
-            final Optional<AttributeType<?>> at = 
Features.toAttribute(property);
-            if (at.isPresent() && 
Geometries.isKnownType(at.get().getValueClass())) {
-                final GenericName name = property.getName();
-                final String attributeName = (property instanceof 
LinkOperation)
-                                             ? ((LinkOperation) 
property).referentName : name.toString();
-                final boolean isDefault = 
AttributeConvention.GEOMETRY_PROPERTY.equals(name);
-                if (isDefault) {
-                    defaultGeometry = attributeName;
+        final var names = new LinkedHashMap<String, 
CoordinateReferenceSystem>(4);
+        for (int i=0; i < geometryAttributes.length; i++) {
+            final String propertyName;          // Name of 
`geometryAttributes[i]`, possibly inherited.
+            final String attributeName;         // Name of the property after 
following the link.
+            CoordinateReferenceSystem attributeCRS = null;
+            final PropertyType property = geometryAttributes[i];
+            if (property == null && inheritFrom != null) {
+                /*
+                 * When this constructor is invoked by 
`updateDependencies(Map)`, a null property means to inherit
+                 * the property at the same index from the previous operation. 
The caller is responsible to ensure
+                 * that the indexes match.
+                 */
+                propertyName = attributeName = inheritFrom.attributeNames[i];
+                if (inheritFrom.attributeToCRS != null) {
+                    final CoordinateOperation op = 
inheritFrom.attributeToCRS[i];
+                    if (op != null) {
+                        attributeCRS = op.getSourceCRS();
+                        characterizedByCRS = true;
+                    }
                 }
-                CoordinateReferenceSystem attributeCRS = null;
+            } else {
+                final AttributeType<?> at = 
Features.toAttribute(property).orElse(null);
+                if (at == null || !Geometries.isKnownType(at.getValueClass())) 
{
+                    continue;   // Not a geometry property. Ignore as per 
method contract.
+                }
+                /*
+                 * If a property is a link to an attribute, then the key will 
be the name of the referenced
+                 * attribute instead of the operation name. This is for 
avoiding to query the same geometry
+                 * twice when the attribute is also specified explicitly in 
the array of properties.
+                 */
+                propertyName  = property.getName().toString();
+                attributeName = 
Features.getLinkTarget(property).orElse(propertyName);
                 /*
-                 * Set `characterizedByCRS` to true if we find at least one 
attribute which may have the
-                 * "CRS" characteristic. Note that we cannot rely on 
`attributeCRS` being non-null
+                 * Set `characterizedByCRS` to `true` if we find at least one 
attribute which have the
+                 * "sis:crs" characteristic. Note that we cannot rely on 
`attributeCRS` being non-null
                  * because an attribute may be characterized by a CRS without 
providing default CRS.
                  */
-                final AttributeType<?> ct = 
at.get().characteristics().get(AttributeConvention.CRS);
+                final AttributeType<?> ct = 
at.characteristics().get(AttributeConvention.CRS);
                 if (ct != null && 
CoordinateReferenceSystem.class.isAssignableFrom(ct.getValueClass())) {
-                    attributeCRS = (CoordinateReferenceSystem) 
ct.getDefaultValue();              // May still null.
-                    if (targetCRS == null && isDefault) {
-                        targetCRS = attributeCRS;
-                    }
+                    attributeCRS = (CoordinateReferenceSystem) 
ct.getDefaultValue();    // May still be null.
                     characterizedByCRS = true;
                 }
-                names.putIfAbsent(attributeName, attributeCRS);
             }
+            /*
+             * If the user did not specified a CRS explicitly, take the CRS of 
the default geometry.
+             * If there is no default geometry, the CRS of the first geometry 
will be taken in next loop.
+             */
+            if (AttributeConvention.GEOMETRY.equals(propertyName)) {
+                defaultGeometry = attributeName;
+                defaultIsFirst = names.isEmpty();
+                if (targetCRS == null) {
+                    targetCRS = attributeCRS;
+                }
+            }
+            names.putIfAbsent(attributeName, attributeCRS);
         }
         /*
          * Copy the names in an array with the default geometry first. If 
possible, find the coordinate operations
-         * now in order to avoid the potentially costly call to 
CRS.findOperation(…) for each feature on which this
-         * EnvelopeOperation will be applied.
+         * now in order to avoid the potentially costly calls to 
`CRS.findOperation(…)` for each feature on which
+         * this `EnvelopeOperation` will be applied.
          */
-        names.remove(null);                                                    
                 // Paranoiac safety.
+        if (!defaultIsFirst) {
+            JDK21.putFirst(names, defaultGeometry, 
names.remove(defaultGeometry));
+        }
+        names.remove(null);    // Paranoiac safety.
         attributeNames = new String[names.size()];
         attributeToCRS = characterizedByCRS ? new 
CoordinateOperation[attributeNames.length] : null;
-        int n = (defaultGeometry == null) ? 0 : 1;
-        for (final Map.Entry<String,CoordinateReferenceSystem> entry : 
names.entrySet()) {
-            final int i;
-            final String name = entry.getKey();
-            if (name.equals(defaultGeometry)) {
-                defaultGeometry = null;
-                i = 0;
-            } else {
-                i = n++;
-            }
-            attributeNames[i] = name;
+        int i = 0;
+        for (final Map.Entry<String, CoordinateReferenceSystem> entry : 
names.entrySet()) {
+            attributeNames[i] = entry.getKey();
             if (characterizedByCRS) {
                 final CoordinateReferenceSystem value = entry.getValue();
                 if (value != null) {
                     if (targetCRS == null) {
-                        targetCRS = value;                  // Fallback if 
default geometry has no CRS.
+                        targetCRS = value;      // Fallback if the default 
geometry has no CRS.
                     }
                     /*
                      * The following operation is often identity. We do not 
filter identity operations
@@ -209,6 +259,7 @@ final class EnvelopeOperation extends AbstractOperation {
                     attributeToCRS[i] = CRS.findOperation(value, targetCRS, 
null);
                 }
             }
+            i++;
         }
         resultType = FeatureOperations.POOL.unique(new DefaultAttributeType<>(
                 resultIdentification(identification), Envelope.class, 1, 1, 
null));
@@ -242,11 +293,37 @@ final class EnvelopeOperation extends AbstractOperation {
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public synchronized Set<String> getDependencies() {
-        if (dependencies == null) {
-            dependencies = CollectionsExt.immutableSet(true, attributeNames);
+    public Set<String> getDependencies() {
+        Set<String> cached = dependencies;
+        if (cached == null) {
+            // Not really a problem if computed twice concurrently.
+            dependencies = cached = CollectionsExt.immutableSet(true, 
attributeNames);
+        }
+        return cached;
+    }
+
+    /**
+     * Returns the same operation but using different properties as inputs.
+     *
+     * @param  dependencies  the new properties to use as operation inputs.
+     * @return the new operation, or {@code this} if unchanged.
+     */
+    @Override
+    public Operation updateDependencies(final Map<String, PropertyType> 
dependencies) {
+        boolean foundAny = false;
+        final var geometryAttributes = new PropertyType[attributeNames.length];
+        for (int i=0; i < geometryAttributes.length; i++) {
+            foundAny |= (geometryAttributes[i] = 
dependencies.get(attributeNames[i])) != null;
+        }
+        if (foundAny) try {
+            var op = new EnvelopeOperation(inherit(), explicitCRS ? targetCRS 
: null, geometryAttributes, this);
+            if (!equals(op)) {
+                return FeatureOperations.POOL.unique(op);
+            }
+        } catch (FactoryException e) {
+            throw new FeatureInstantiationException(e.getMessage(), e);
         }
-        return dependencies;
+        return this;
     }
 
     /**
@@ -335,7 +412,7 @@ final class EnvelopeOperation extends AbstractOperation {
                          * a CRS characteristic is associated to a particular 
feature, setting `op` to null
                          * will cause a new coordinate operation to be 
searched.
                          */
-                        final Attribute<?> at = ((Attribute<?>) 
feature.getProperty(attributeNames[i]))
+                        final var at = ((Attribute<?>) 
feature.getProperty(attributeNames[i]))
                                 
.characteristics().get(AttributeConvention.CRS);
                         final Object geomCRS;
                         if (at != null && (geomCRS = at.getValue()) != null) {
@@ -413,10 +490,11 @@ final class EnvelopeOperation extends AbstractOperation {
     public boolean equals(final Object obj) {
         if (super.equals(obj)) {
             // `this.result` is compared (indirectly) by the super class.
-            final EnvelopeOperation that = (EnvelopeOperation) obj;
+            final var that = (EnvelopeOperation) obj;
             return Arrays.equals(attributeNames, that.attributeNames) &&
                    Arrays.equals(attributeToCRS, that.attributeToCRS) &&
-                   Objects.equals(targetCRS,     that.targetCRS);
+                   Objects.equals(targetCRS,     that.targetCRS) &&
+                   explicitCRS == that.explicitCRS;
         }
         return false;
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
index 05210b25e4..1b9bde711c 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
@@ -215,25 +215,25 @@ public final class FeatureOperations extends Static {
                 }
             }
         }
-        return POOL.unique(new StringJoinOperation(identification, delimiter, 
prefix, suffix, singleAttributes));
+        return POOL.unique(new StringJoinOperation(identification, delimiter, 
prefix, suffix, singleAttributes, null));
     }
 
     /**
      * Creates an operation computing the envelope that encompass all 
geometries found in the given attributes.
-     * Geometries can be in different coordinate reference systems; they will 
be transformed to the first non-null
-     * CRS in the following choices:
+     * Geometries can be in different coordinate reference systems, in which 
case they will be transformed to
+     * the first non-null <abbr>CRS</abbr> in the following choices:
      *
      * <ol>
-     *   <li>the CRS specified to this method,</li>
-     *   <li>the CRS of the default geometry, or</li>
-     *   <li>the CRS of the first non-empty geometry.</li>
+     *   <li>the <abbr>CRS</abbr> specified to this method,</li>
+     *   <li>the <abbr>CRS</abbr> of the default geometry, or</li>
+     *   <li>the <abbr>CRS</abbr> of the first non-empty geometry.</li>
      * </ol>
      *
      * The {@linkplain AbstractOperation#getResult() result} of this operation 
is an {@code Attribute}
      * with values of type {@link org.opengis.geometry.Envelope}. If the 
{@code crs} argument given to
      * this method is non-null, then the
      * {@linkplain 
org.apache.sis.geometry.GeneralEnvelope#getCoordinateReferenceSystem() envelope 
CRS}
-     * will be that CRS.
+     * will be that <abbr>CRS</abbr>.
      *
      * <h4>Limitations</h4>
      * If a geometry contains other geometries, this operation queries only 
the envelope of the root geometry.
@@ -254,13 +254,13 @@ public final class FeatureOperations extends Static {
             final PropertyType... geometryAttributes) throws FactoryException
     {
         ArgumentChecks.ensureNonNull("geometryAttributes", geometryAttributes);
-        return POOL.unique(new EnvelopeOperation(identification, crs, 
geometryAttributes));
+        return POOL.unique(new EnvelopeOperation(identification, crs, 
geometryAttributes, null));
     }
 
     /**
      * Creates a single geometry from a sequence of points or polylines stored 
in another property.
      * When evaluated, this operation reads a feature property containing a 
sequence of {@code Point}s or {@code Polyline}s.
-     * Those geometries shall be instances of the specified geometry library 
(e.g. JTS or ESRI).
+     * Those geometries shall be instances of the specified geometry library 
(e.g. <abbr>JTS</abbr> or <abbr>ESRI</abbr>).
      * The merged geometry is usually a {@code Polyline},
      * unless the sequence of source geometries is empty or contains a single 
element.
      * The merged geometry is re-computed every time that the operation is 
evaluated.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
index 00ad38f9d1..9e7de95a16 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
@@ -134,7 +134,7 @@ public final class Features extends Static {
      *       result type} is another operation, then the above check is 
performed recursively.</li>
      * </ul>
      *
-     * @param  type  the data type to express as an attribute type.
+     * @param  type  the data type to express as an attribute type, or {@code 
null}.
      * @return the attribute type, or empty if this method cannot find any.
      *
      * @since 1.1
@@ -156,7 +156,7 @@ public final class Features extends Static {
      *       result type} is another operation, then the above check is 
performed recursively.</li>
      * </ul>
      *
-     * @param  type  the data type to express as an attribute type.
+     * @param  type  the data type to express as an attribute type, or {@code 
null}.
      * @return the association role, or empty if this method cannot find any.
      *
      * @since 1.4
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/GroupAsPolylineOperation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/GroupAsPolylineOperation.java
index 242418ac1c..7f603f4ca3 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/GroupAsPolylineOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/GroupAsPolylineOperation.java
@@ -17,7 +17,7 @@
 package org.apache.sis.feature;
 
 import java.util.Map;
-import java.util.Collection;
+import java.util.Set;
 import java.util.Iterator;
 import java.util.EnumMap;
 import org.opengis.parameter.ParameterDescriptorGroup;
@@ -28,6 +28,7 @@ import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryType;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.setup.GeometryLibrary;
+import org.apache.sis.util.privy.CollectionsExt;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.feature.Feature;
@@ -86,7 +87,7 @@ final class GroupAsPolylineOperation extends 
AbstractOperation {
      * @param  library         the library providing the implementations of 
geometry objects to read and write.
      * @param  components      attribute, association or operation providing 
the geometries to group as a polyline.
      */
-    static Operation create(final Map<String,?> identification, final 
GeometryLibrary library, PropertyType components) {
+    static AbstractOperation create(final Map<String,?> identification, final 
GeometryLibrary library, PropertyType components) {
         if (components instanceof LinkOperation) {
             components = ((LinkOperation) components).result;
         }
@@ -97,8 +98,7 @@ final class GroupAsPolylineOperation extends 
AbstractOperation {
             }
             isFeatureAssociation = false;
         } else {
-            isFeatureAssociation = (components instanceof 
FeatureAssociationRole)
-                    && ((FeatureAssociationRole) 
components).getMaximumOccurs() == 1;
+            isFeatureAssociation = (components instanceof 
FeatureAssociationRole);
             if (!isFeatureAssociation) {
                 throw new 
IllegalArgumentException(Resources.format(Resources.Keys.IllegalPropertyType_2,
                                                    components.getName(), 
components.getClass()));
@@ -129,6 +129,32 @@ final class GroupAsPolylineOperation extends 
AbstractOperation {
         return EMPTY_PARAMS;
     }
 
+    /**
+     * Returns the names of feature properties that this operation needs for 
performing its task.
+     */
+    @Override
+    public Set<String> getDependencies() {
+        return Set.of(propertyName);
+    }
+
+    /**
+     * Returns the same operation but using different properties as inputs.
+     *
+     * @param  dependencies  the new properties to use as operation inputs.
+     * @return the new operation, or {@code this} if unchanged.
+     */
+    @Override
+    public Operation updateDependencies(final Map<String, PropertyType> 
dependencies) {
+        final PropertyType target = dependencies.get(propertyName);
+        if (target != null) {
+            final AbstractOperation op = create(inherit(), geometries.library, 
target);
+            if (!equals(op)) {
+                return FeatureOperations.POOL.unique(op);
+            }
+        }
+        return this;
+    }
+
     /**
      * Returns the expected result type.
      */
@@ -199,10 +225,10 @@ final class GroupAsPolylineOperation extends 
AbstractOperation {
          */
         private G compute() {
             /*
-             * Cast to `Collection` should be safe if the constructor
-             * ensured that `Features.getMaximumOccurs(property) > 1`.
+             * The property value is usually cast directly to `Collection` 
when the
+             * constructor ensured that `Features.getMaximumOccurs(property) > 
1`.
              */
-            Iterator<?> paths = ((Collection<?>) 
feature.getPropertyValue(propertyName)).iterator();
+            Iterator<?> paths = 
CollectionsExt.toCollection(feature.getPropertyValue(propertyName)).iterator();
             if (isFeatureAssociation) {
                 final Iterator<?> it = paths;
                 paths = new Iterator<Object>() {
@@ -240,7 +266,7 @@ final class GroupAsPolylineOperation extends 
AbstractOperation {
     @Override
     public boolean equals(final Object obj) {
         if (super.equals(obj)) {
-            final GroupAsPolylineOperation that = (GroupAsPolylineOperation) 
obj;
+            final var that = (GroupAsPolylineOperation) obj;
             return propertyName.equals(that.propertyName) &&
                    geometries.equals(that.geometries);
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
index 9bf239939c..63da5f3233 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java
@@ -26,6 +26,7 @@ import org.apache.sis.util.resources.Errors;
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.feature.Feature;
 import org.opengis.feature.IdentifiedType;
+import org.opengis.feature.Operation;
 import org.opengis.feature.Property;
 import org.opengis.feature.PropertyType;
 
@@ -103,6 +104,21 @@ final class LinkOperation extends AbstractOperation {
         return Set.of(referentName);
     }
 
+    /**
+     * Returns the same operation but using different properties as inputs.
+     *
+     * @param  dependencies  the new properties to use as operation inputs.
+     * @return the new operation, or {@code this} if unchanged.
+     */
+    @Override
+    public Operation updateDependencies(final Map<String, PropertyType> 
dependencies) {
+        final PropertyType target = dependencies.get(referentName);
+        if (target == null || target.equals(result)) {
+            return this;
+        }
+        return FeatureOperations.POOL.unique(new LinkOperation(inherit(), 
target));
+    }
+
     /**
      * Returns the property from the referenced attribute of feature 
association.
      *
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
index 703c02faac..c1c035a7dd 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
@@ -151,8 +151,11 @@ final class StringJoinOperation extends AbstractOperation {
 
     /**
      * The property names as an unmodifiable set, created when first needed.
+     * This is simply {@link #attributeNames} copied in a unmodifiable set.
+     *
+     * @see #getDependencies()
      */
-    private transient Set<String> dependencies;
+    private transient volatile Set<String> dependencies;
 
     /**
      * The type of the result returned by the string concatenation operation.
@@ -180,6 +183,12 @@ final class StringJoinOperation extends AbstractOperation {
      * It is caller's responsibility to ensure that {@code delimiter} and 
{@code singleAttributes} are not null.
      * This private constructor does not verify that condition on the 
assumption that the public API did.
      *
+     * @param  identification    the name and other information to be given to 
this operation.
+     * @param  delimiter         the characters to use as delimiter between 
each single property value.
+     * @param  prefix            characters to use at the beginning of the 
concatenated string, or {@code null} if none.
+     * @param  suffix            characters to use at the end of the 
concatenated string, or {@code null} if none.
+     * @param  singleAttributes  identification of the single attributes (or 
operations producing attributes) to concatenate.
+     * @param  inheritFrom       existing operation from which to inherit null 
attributes, or {@code null} if none.
      * @throws UnconvertibleObjectException if at least one attributes is not 
convertible from a string.
      * @throws IllegalArgumentException if the operation failed for another 
reason.
      *
@@ -187,7 +196,8 @@ final class StringJoinOperation extends AbstractOperation {
      */
     @SuppressWarnings({"rawtypes", "unchecked"})                               
         // Generic array creation.
     StringJoinOperation(final Map<String,?> identification, final String 
delimiter,
-            final String prefix, final String suffix, final PropertyType[] 
singleAttributes)
+            final String prefix, final String suffix, final PropertyType[] 
singleAttributes,
+            final StringJoinOperation inheritFrom)
     {
         super(identification);
         attributeNames = new String[singleAttributes.length];
@@ -206,13 +216,19 @@ final class StringJoinOperation extends AbstractOperation 
{
              * combinations (e.g. operation producing an association).
              */
             IdentifiedType propertyType = singleAttributes[i];
-            ArgumentChecks.ensureNonNullElement("singleAttributes", i, 
propertyType);
+            if (inheritFrom == null) {
+                ArgumentChecks.ensureNonNullElement("singleAttributes", i, 
propertyType);
+            } else if (propertyType == null) {
+                attributeNames[i] = inheritFrom.attributeNames[i];
+                converters[i] = inheritFrom.converters[i];
+                continue;
+            }
             final GenericName name = propertyType.getName();
             int maximumOccurs = 0;                              // May be a 
bitwise combination; need only to know if > 1.
             PropertyNotFoundException cause = null;             // In case of 
failure to find "sis:identifier" property.
             final boolean isAssociation = (propertyType instanceof 
FeatureAssociationRole);
             if (isAssociation) {
-                final FeatureAssociationRole role = (FeatureAssociationRole) 
propertyType;
+                final var role = (FeatureAssociationRole) propertyType;
                 final FeatureType ft = role.getValueType();
                 maximumOccurs = role.getMaximumOccurs();
                 try {
@@ -253,8 +269,12 @@ final class StringJoinOperation extends AbstractOperation {
             }
             converters[i] = converter;
         }
-        resultType = FeatureOperations.POOL.unique(new DefaultAttributeType<>(
-                resultIdentification(identification), String.class, 1, 1, 
null));
+        if (inheritFrom != null) {
+            resultType = inheritFrom.resultType;
+        } else {
+            resultType = FeatureOperations.POOL.unique(new 
DefaultAttributeType<>(
+                    resultIdentification(identification), String.class, 1, 1, 
null));
+        }
         this.delimiter = delimiter;
         this.prefix = (prefix == null) ? "" : prefix;
         this.suffix = (suffix == null) ? "" : suffix;
@@ -282,25 +302,49 @@ final class StringJoinOperation extends AbstractOperation 
{
         return resultType;
     }
 
+    /**
+     * Returns the name of the properties from which to get the values to 
concatenate.
+     * This is the same information as {@link #getDependencies()}, only in a 
different
+     * kind of collection.
+     */
+    final List<String> getAttributeNames() {
+        return UnmodifiableArrayList.wrap(attributeNames);
+    }
+
     /**
      * Returns the names of feature properties that this operation needs for 
performing its task.
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")
-    public synchronized Set<String> getDependencies() {
-        if (dependencies == null) {
-            dependencies = CollectionsExt.immutableSet(true, attributeNames);
+    public Set<String> getDependencies() {
+        Set<String> cached = dependencies;
+        if (cached == null) {
+            // Not really a problem if computed twice concurrently.
+            dependencies = cached = CollectionsExt.immutableSet(true, 
attributeNames);
         }
-        return dependencies;
+        return cached;
     }
 
     /**
-     * Returns the name of the properties from which to get the values to 
concatenate.
-     * This is the same information as {@link #getDependencies()}, only in a 
different
-     * kind of collection.
+     * Returns the same operation but using different properties as inputs.
+     *
+     * @param  dependencies  the new properties to use as operation inputs.
+     * @return the new operation, or {@code this} if unchanged.
      */
-    final List<String> getAttributeNames() {
-        return UnmodifiableArrayList.wrap(attributeNames);
+    @Override
+    public Operation updateDependencies(final Map<String, PropertyType> 
dependencies) {
+        boolean hasNonNull = false;
+        final var singleAttributes = new PropertyType[attributeNames.length];
+        for (int i=0; i < singleAttributes.length; i++) {
+            hasNonNull |= (singleAttributes[i] = 
dependencies.get(attributeNames[i])) != null;
+        }
+        if (hasNonNull) {
+            final var op = new StringJoinOperation(inherit(), delimiter, 
prefix, suffix, singleAttributes, this);
+            if (!(Arrays.equals(op.attributeNames, attributeNames) && 
Arrays.equals(op.converters, converters))) {
+                return FeatureOperations.POOL.unique(op);
+            }
+        }
+        return this;
     }
 
     /**
@@ -356,7 +400,7 @@ final class StringJoinOperation extends AbstractOperation {
          */
         @Override
         public String getValue() throws UnconvertibleObjectException {
-            final StringBuilder sb = new StringBuilder();
+            final var sb = new StringBuilder();
             String sep = prefix;
             String name  = null;
             Object value = null;
@@ -421,7 +465,7 @@ final class StringJoinOperation extends AbstractOperation {
              * read them (no need to store the substrings) but do not store 
them in the properties
              * before we succeeded to parse all values, so we have a "all or 
nothing" behavior.
              */
-            final Object[] values = new Object[attributeNames.length];
+            final var values = new Object[attributeNames.length];
             int lower = prefix.length();
             int upper = lower;
             int count = 0;
@@ -520,7 +564,7 @@ final class StringJoinOperation extends AbstractOperation {
     public boolean equals(final Object obj) {
         if (super.equals(obj)) {
             // 'this.result' is compared (indirectly) by the super class.
-            final StringJoinOperation that = (StringJoinOperation) obj;
+            final var that = (StringJoinOperation) obj;
             return Arrays.equals(this.attributeNames, that.attributeNames) &&
                    Arrays.equals(this.converters,     that.converters)     &&
                   Objects.equals(this.delimiter,      that.delimiter)      &&
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
index 88a470d172..7a6a656f7e 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
@@ -16,15 +16,14 @@
  */
 package org.apache.sis.feature.builder;
 
+import java.util.HashMap;
 import java.util.Objects;
 import org.opengis.util.GenericName;
-import org.apache.sis.feature.Features;
-import org.apache.sis.feature.FeatureOperations;
+import org.apache.sis.feature.AbstractOperation;
 import org.apache.sis.util.resources.Errors;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.feature.PropertyType;
-import org.opengis.feature.Operation;
 
 
 /**
@@ -61,35 +60,37 @@ final class OperationWrapper extends PropertyTypeBuilder {
 
     /**
      * Returns the operation or an updated version of the operation.
-     * Updated versions are created only for some kinds of operation, 
described below.
+     * Updated versions are created for some kinds of operation, described 
below.
      * Otherwise, this method returns the same value as {@link #build()}.
      *
-     * <h4>Links</h4>
+     * <h4>Updated operations</h4>
      * If the operation is a link to another property of the feature to build, 
the result type
      * of the original operation is replaced by the target of the link in the 
feature to build.
      * Even if the attribute name is the same, sometime the value class or 
some characteristics
-     * are different.
+     * are different. Similar updates may also be applied to other kinds of 
operation.
      *
      * @throws IllegalStateException if the builder contains inconsistent 
information.
      */
     @Override
     final PropertyType buildForFeature() {
         final FeatureTypeBuilder owner = owner();
-        try {
-            return Features.getLinkTarget(operation).<PropertyType>map((name) 
-> {
-                final PropertyTypeBuilder target = owner.getProperty(name);
+        if (operation instanceof AbstractOperation) {
+            final var op = (AbstractOperation) operation;
+            final var dependencies = new HashMap<String, PropertyType>();
+            for (final String name : op.getDependencies()) {
+                final PropertyTypeBuilder target;
+                try {
+                    target = owner.getProperty(name);
+                } catch (IllegalArgumentException e) {
+                    throw new IllegalStateException(e.getMessage(), e);
+                }
                 if (target != null) {
-                    final PropertyType result = target.build();
-                    if (!result.equals(((Operation) operation).getResult())) {
-                        initialize(operation);
-                        return FeatureOperations.link(identification(), 
result);
-                    }
+                    dependencies.put(name, target.build());
                 }
-                return null;
-            }).orElse(operation);
-        } catch (IllegalArgumentException e) {
-            throw new IllegalStateException(e.getMessage(), e);
+            }
+            return op.updateDependencies(dependencies);
         }
+        return operation;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
index b887eda202..93914b7037 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
@@ -134,6 +134,11 @@ public class Resources extends IndexedResourceBundle {
          */
         public static final short CanNotVisit_2 = 77;
 
+        /**
+         * Cannot rename the “{0}” property as “{1}” because it is used by an 
operation.
+         */
+        public static final short CannotRenameDependency_2 = 93;
+
         /**
          * The two categories “{0}” and “{2}” have overlapping ranges: {1} and 
{3} respectively.
          */
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
index 2dd17c3f46..f03d695a80 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
@@ -30,6 +30,7 @@ CanNotCreateTwoDimensionalCRS_1   = Cannot create a 
two-dimensional reference sy
 CanNotEnumerateValuesInRange_1    = Cannot enumerate values in the {0} range.
 CanNotInstantiateProperty_1       = Property \u201c{0}\u201d is not a type 
that can be instantiated.
 CanNotMapToGridDimensions         = Some envelope dimensions cannot be mapped 
to grid dimensions.
+CannotRenameDependency_2          = Cannot rename the \u201c{0}\u201d property 
as \u201c{1}\u201d because it is used by an operation.
 CanNotSetCharacteristics_2        = Cannot set a value of type \u2018{1}\u2019 
to characteristic \u201c{0}\u201d.
 CanNotSetDerivedGridProperty_1    = Cannot set this derived grid property 
after a call to \u201c{0}\u201d method.
 CanNotSetPropertyValue_1          = Type of the \u201c{0}\u201d property does 
not allow to set a value.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
index 13996cde79..fa880920db 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
@@ -35,6 +35,7 @@ CanNotCreateTwoDimensionalCRS_1   = Ne peut pas cr\u00e9er un 
syst\u00e8me de r\
 CanNotEnumerateValuesInRange_1    = Ne peut pas \u00e9num\u00e9rer les valeurs 
dans la plage {0}.
 CanNotInstantiateProperty_1       = La propri\u00e9t\u00e9 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas d\u2019un type qui peut \u00eatre 
instanci\u00e9.
 CanNotMapToGridDimensions         = Certaines dimensions de l\u2019enveloppe 
ne correspondent pas \u00e0 des dimensions de la grille.
+CannotRenameDependency_2          = Ne peut pas renommer la 
propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb comme 
\u00ab\u202f{1}\u202f\u00bb parce qu\u2019elle est utilis\u00e9e par une 
op\u00e9ration.
 CanNotSetCharacteristics_2        = Ne peut pas assigner une valeur de type 
\u2018{1}\u2019 \u00e0 la caract\u00e9ristique \u00ab\u202f{0}\u202f\u00bb.
 CanNotSetDerivedGridProperty_1    = Ne peut pas d\u00e9finir cette 
propri\u00e9t\u00e9 de la grille d\u00e9riv\u00e9e apr\u00e8s un appel \u00e0 
la m\u00e9thode \u00ab\u202f{0}\u202f\u00bb.
 CanNotSetPropertyValue_1          = Le type de la propri\u00e9t\u00e9 
\u00ab\u202f{0}\u202f\u00bb ne permet pas de d\u00e9finir une valeur.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
index c2dcf01e13..51ab93d85f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
@@ -20,6 +20,7 @@ import java.util.Map;
 import java.util.HashMap;
 import java.util.List;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Iterator;
 import java.util.Locale;
 import java.util.Objects;
@@ -34,6 +35,7 @@ import org.apache.sis.feature.builder.AssociationRoleBuilder;
 import org.apache.sis.feature.builder.AttributeTypeBuilder;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.feature.builder.PropertyTypeBuilder;
+import org.apache.sis.feature.internal.Resources;
 import org.apache.sis.util.ArgumentCheckByAssertion;
 import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.util.privy.Strings;
@@ -197,13 +199,13 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
      * This method may return {@code null} if it cannot resolve the property 
type, in which case
      * the caller should throw an exception (throwing an exception is left to 
the caller because
      * it can produces a better error message). Operation's dependencies, if 
any, are added into
-     * the given {@code deferred} list.
+     * the given {@code deferred} set.
      *
      * @param  property  the {@linkplain #source} property to add.
      * @param  deferred  where to add operation's dependencies, or {@code 
null} for not collecting dependencies.
      * @return builder for the projected property, or {@code null} if it 
cannot be resolved.
      */
-    private PropertyTypeBuilder addPropertyResult(PropertyType property, final 
List<String> deferred) {
+    private PropertyTypeBuilder addPropertyResult(PropertyType property, final 
Collection<String> deferred) {
         if (property instanceof Operation) {
             final GenericName name = property.getName();
             do {
@@ -246,7 +248,7 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
             return null;
         }
         final PropertyTypeBuilder builder;
-        List<String> deferred;
+        final Collection<String> deferred;
         if (sourceIsDependency) {
             /*
              * Adding a property which is not defined in the feature type 
specified at construction time,
@@ -257,19 +259,17 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
             reserve(property.getName(), null);
             deferred = new ArrayList<>();
             builder = addPropertyResult(property, deferred);
-        } else {
+        } else if (property instanceof AbstractOperation) {
             /*
-             * For link operations, remember the dependencies in order to 
determine (after we added all properties)
+             * For operations, remember the dependencies in order to determine 
(after we added all properties)
              * if we can keep the property as an operation or if we will need 
to copy the value in an attribute.
-             * For other kind of operations, unconditionally replace the 
operation by its result.
+             * If the operation is not an `AbstractOperation`, unconditionally 
replace operation by its result.
              */
-            deferred = Features.getLinkTargets(property);
-            if (deferred.isEmpty()) {
-                deferred = new ArrayList<>();
-                builder = addPropertyResult(property, deferred);
-            } else {
-                builder = addProperty(property);
-            }
+            deferred = ((AbstractOperation) property).getDependencies();
+            builder = addProperty(property);
+        } else {
+            deferred = new ArrayList<>();
+            builder = addPropertyResult(property, deferred);
         }
         final var item = new Item(named ? property.getName() : null, builder);
         requested.add(item);
@@ -338,9 +338,9 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
         private boolean preferCurrentName;
 
         /**
-         * Whether this property needs at least one dependency which is not 
included in the list of properties
-         * requested by the user. In such case, we cannot keep the link 
operation and need to replace the link
-         * by a stored attribute.
+         * Whether this property is an operation having at least one 
dependency which is not included
+         * in the list of properties requested by the user. In such case, we 
cannot keep the operation
+         * and need to replace it by a stored attribute.
          *
          * @see #replaceIfMissingDependency()
          */
@@ -453,7 +453,10 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
                         final Class<?> c = ((AttributeType<?>) 
result).getValueClass();
                         final Class<?> r = type.apply(c);
                         if (r != null) {
-                            // We can be lenient for link operation, but must 
be strict for other operations.
+                            /*
+                             * We can be lenient for link operation, but must 
be strict for other operations.
+                             * Example: a link to a geometry, but relaxing the 
`Polygon` type to `Geometry`.
+                             */
                             if (Features.getLinkTarget(property).isPresent() ? 
r.isAssignableFrom(c) : r.equals(c)) {
                                 return true;
                             }
@@ -630,6 +633,7 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
      * The elements added into {@code deferred} are {@linkplain #source} 
properties.
      *
      * @param  deferred  where to add missing transitive dependencies (source 
properties).
+     * @throws UnsupportedOperationException if there is an attempt to rename 
a property which is used by an operation.
      */
     private void resolveDependencies(final List<PropertyType> deferred) {
         final var it = dependencies.entrySet().iterator();
@@ -640,12 +644,8 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
             Item item = reservedNames.get(sourceName);
             if (item != null) {
                 if (!sourceName.equals(item.sourceName)) {
-                    /*
-                     * If we want to support that feature in a future version, 
we would need a `replace` method
-                     * for replacing a builder at a specific index or for a 
specific property name. A difficulty
-                     * is that for compound identifiers, we have no API for 
reusing the same prefix and suffix.
-                     */
-                    throw new UnsupportedOperationException("Renaming of 
properties used in links is not yet supported.");
+                    throw new 
UnsupportedOperationException(Resources.forLocale(getLocale())
+                            
.getString(Resources.Keys.CannotRenameDependency_2, item.sourceName, 
sourceName));
                 }
             } else {
                 for (Item dependent : entry.getValue()) {
@@ -698,11 +698,12 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
      * for meaning that this projection does nothing.
      *
      * @return the feature types with and without dependencies, or empty if 
there is no projection.
+     * @throws UnsupportedOperationException if there is an attempt to rename 
a property which is used by an operation.
      */
     public Optional<FeatureProjection> project() {
         requested.forEach(Item::validateName);
         /*
-         * Add properties for all dependencies that are required by link 
operations but are not already present.
+         * Add properties for all dependencies that are required by operations 
but are not already present.
          * If there is no need to add anything, `typeWithDependencies` will be 
directly the feature type to return.
          */
         final List<PropertyTypeBuilder> properties = properties();
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
index 80dec5de00..4412e94419 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
@@ -43,6 +43,7 @@ import org.apache.sis.pending.jdk.JDK19;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.Emptiable;
+import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.util.iso.Names;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -51,6 +52,7 @@ import org.opengis.feature.FeatureType;
 import org.opengis.feature.Attribute;
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.Operation;
+import org.opengis.feature.PropertyNotFoundException;
 import org.opengis.filter.FilterFactory;
 import org.opengis.filter.Filter;
 import org.opengis.filter.Expression;
@@ -619,6 +621,9 @@ public class FeatureQuery extends Query implements 
Cloneable, Emptiable, Seriali
          *
          * @param  builder  the builder where to add the property.
          * @return whether the property has been successfully added.
+         * @throws InvalidFilterValueException if {@linkplain #expression} is 
invalid.
+         * @throws PropertyNotFoundException if the property was not found in 
{@code builder.source()}.
+         * @throws UnconvertibleObjectException if the property default value 
cannot be converted to the expected type.
          */
         final boolean addTo(final FeatureProjectionBuilder builder) {
             final FeatureExpression<? super Feature, ?> fex = 
FeatureExpression.castOrCopy(expression);
@@ -743,8 +748,12 @@ public class FeatureQuery extends Query implements 
Cloneable, Emptiable, Seriali
      *   <li>Otherwise the localized string "Unnamed #1" with increasing 
numbers.</li>
      * </ul>
      *
-     * @param sourceType  the feature type to project.
-     * @param locale      locale for error messages, or {@code null} for the 
default locale.
+     * @param  sourceType  the feature type to project.
+     * @param  locale      locale for error messages, or {@code null} for the 
default locale.
+     * @throws InvalidFilterValueException if an {@linkplain 
NamedExpression#expression expression} is invalid.
+     * @throws PropertyNotFoundException if a property referenced by an 
expression was not found in {@code sourceType}.
+     * @throws UnconvertibleObjectException if a property default value cannot 
be converted to the expected type.
+     * @throws UnsupportedOperationException if there is an attempt to rename 
a property which is used by an operation.
      */
     final Optional<FeatureProjection> project(final FeatureType sourceType, 
final Locale locale) {
         if (projection == null) {
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSet.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSet.java
index b500df4042..29b6fad9f1 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSet.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSet.java
@@ -101,7 +101,6 @@ public interface FeatureSet extends DataSet {
      * @param  query  definition of feature and feature properties filtering 
applied at reading time.
      * @return resulting subset of features (never {@code null}).
      * @throws UnsupportedQueryException if this {@code FeatureSet} cannot 
execute the given query.
-     *         This includes query validation errors.
      * @throws DataStoreException if another error occurred while processing 
the query.
      *
      * @see GridCoverageResource#subset(CoverageQuerty)
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
index c4d4e792a7..3838769fa7 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
@@ -97,8 +97,8 @@ final class FeatureSubset extends AbstractFeatureSet {
             try {
                 projection = query.project(type, 
listeners.getLocale()).orElse(null);
                 resultType = (projection != null) ? projection.typeRequested : 
type;
-            } catch (IllegalArgumentException e) {
-                throw new 
DataStoreContentException(Resources.forLocale(listeners.getLocale())
+            } catch (RuntimeException e) {      // Too many exceptions for 
listing them all.
+                throw new 
UnsupportedQueryException(Resources.forLocale(listeners.getLocale())
                         
.getString(Resources.Keys.CanNotDeriveTypeFromFeature_1, type.getName()), e);
             }
         }
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/UnsupportedQueryException.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/UnsupportedQueryException.java
index 1444533915..b0a5d879ac 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/UnsupportedQueryException.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/UnsupportedQueryException.java
@@ -19,6 +19,18 @@ package org.apache.sis.storage;
 
 /**
  * Thrown when a resources cannot be filtered with a given query.
+ * Some examples of cases where this exception may be thrown are:
+ *
+ * <ul>
+ *   <li>The {@link Query} is an instance of a class unsupported by the 
resource
+ *       (for example, applying a {@link CoverageQuery} on a {@link 
FeatureSet}).</li>
+ *   <li>The query is requesting a property that does not exist in the {@link 
Resource}.</li>
+ *   <li>The values in the {@link DataStore} are unconvertible to some 
characteristics
+ *       (e.g., type or name) requested by the query.</li>
+ * </ul>
+ *
+ * This exception may be thrown when {@link FeatureSet#subset(Query)} is 
executed,
+ * but may also be deferred until another method of the returned subset is 
invoked.
  *
  * @author  Johann Sorel (Geomatys)
  * @version 0.8
diff --git 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
index 6bd071f8f3..80ea1a7acf 100644
--- 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
+++ 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
@@ -459,7 +459,7 @@ public final class FeatureQueryTest extends TestCase {
                             virtualProjection(ff.property("valueMissing", 
Integer.class), "renamed1"));
         assertXPathsEqual("value1", "valueMissing");
 
-        var exception = assertThrows(DataStoreContentException.class, 
this::executeAndGetFirst);
+        var exception = assertThrows(UnsupportedQueryException.class, 
this::executeAndGetFirst);
         assertMessageContains(exception);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java
index d77a06786f..c1a56e0023 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/pending/jdk/JDK21.java
@@ -21,6 +21,7 @@ import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 import java.util.ListIterator;
+import java.util.LinkedHashMap;
 import java.util.NoSuchElementException;
 
 
@@ -40,8 +41,8 @@ public final class JDK21 {
      * Placeholder for {@code SequencedCollection.getFirst()}.
      *
      * @param  <E>        type of elements in the collection.
-     * @param  sequenced  the sequenced collection for which to get elements 
in reverse order.
-     * @return elements of the given collection in reverse order.
+     * @param  sequenced  the sequenced collection for which from which to get 
an element.
+     * @return the requested element.
      */
     public static <E> E getFirst(final List<E> sequenced) {
         try {
@@ -55,8 +56,8 @@ public final class JDK21 {
      * Placeholder for {@code SequencedCollection.getLast()}.
      *
      * @param  <E>        type of elements in the collection.
-     * @param  sequenced  the sequenced collection for which to get elements 
in reverse order.
-     * @return elements of the given collection in reverse order.
+     * @param  sequenced  the sequenced collection for which from which to get 
an element.
+     * @return the requested element.
      */
     public static <E> E getLast(final List<E> sequenced) {
         try {
@@ -66,6 +67,21 @@ public final class JDK21 {
         }
     }
 
+    /**
+     * Placeholder for {@code SequencedMap.putFirst(K, V)}.
+     *
+     * @param  <K>        type of keys in the map.
+     * @param  <V>        type of values in the map.
+     * @param  sequenced  the sequenced map for which to put an element first.
+     */
+    public static <K,V> void putFirst(final LinkedHashMap<K,V> sequenced, 
final K key, final V value) {
+        @SuppressWarnings("unchecked")
+        final var copy = (LinkedHashMap<K,V>) sequenced.clone();
+        sequenced.clear();
+        sequenced.put(key, value);
+        sequenced.putAll(copy);
+    }
+
     /**
      * Placeholder for {@code SequencedCollection.reversed()}.
      *
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java
index 9d2068257a..1b84986236 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/CollectionsExt.java
@@ -303,7 +303,7 @@ public final class CollectionsExt extends Static {
         final Class<?> valueType = value.getClass();
         if (valueType.isArray()) {
             if (type.isAssignableFrom(valueType)) {
-                final Set<E> set = new LinkedHashSet<>(Arrays.asList((E[]) 
value));
+                final var set = new LinkedHashSet<E>(Arrays.asList((E[]) 
value));
                 set.remove(null);
                 return set.toArray(emptyArray);
             }
@@ -725,15 +725,15 @@ public final class CollectionsExt extends Static {
             }
         }
         if (value instanceof Iterable<?>) {
-            final List<Object> list = new ArrayList<>();
+            final var list = new ArrayList<Object>();
             for (final Object element : (Iterable<?>) value) {
                 list.add(element);
             }
             return list;
         }
         if (value instanceof Iterator<?>) {
-            final Iterator<?> it = (Iterator<?>) value;
-            final List<Object> list = new ArrayList<>();
+            final var it = (Iterator<?>) value;
+            final var list = new ArrayList<Object>();
             while (it.hasNext()) {
                 list.add(it.next());
             }

Reply via email to