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

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

commit 3d377745a9cbb746e07f482fe71082dcfa657854
Merge: ac4ad3f36a ffa96dbde4
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Wed May 21 18:14:30 2025 +0200

    Merge branch 'geoapi-3.1'.
    Contains improvements in: `SQLStore`, ShapeFile (incubator), conversions 
from AWT shapes to JTS, partial DuckDB support, and use of HTTP ranges in 
GeoHEIF.

 .../apache/sis/cloud/aws/s3/ClientFileSystem.java  |   2 +-
 .../org.apache.sis.feature/main/module-info.java   |   1 +
 .../org/apache/sis/feature/AbstractOperation.java  |   4 +-
 .../apache/sis/feature/ExpressionOperation.java    |   2 +-
 .../org/apache/sis/feature/FeatureOperations.java  |  77 ++-
 .../main/org/apache/sis/feature/Features.java      |  25 +-
 .../apache/sis/feature/StringJoinOperation.java    |  14 +
 .../sis/feature/builder/AttributeTypeBuilder.java  |  14 +-
 .../sis/feature/builder/FeatureTypeBuilder.java    |  10 +-
 .../sis/feature/builder/OperationWrapper.java      |  30 +-
 .../sis/feature/builder/PropertyTypeBuilder.java   |  32 +-
 .../apache/sis/feature/builder/TypeBuilder.java    |   2 +-
 .../org/apache/sis/feature/internal/Resources.java |   5 +
 .../sis/feature/internal/Resources.properties      |   1 +
 .../sis/feature/internal/Resources_fr.properties   |   1 +
 .../main/org/apache/sis/feature/package-info.java  |   2 +-
 .../sis/feature/privy/FeatureExpression.java       |  83 ++-
 .../sis/feature/privy/FeatureProjection.java       | 341 ++++++++++
 .../feature/privy/FeatureProjectionBuilder.java    | 739 +++++++++++++++++++++
 .../org/apache/sis/feature/privy/FeatureView.java  | 129 ++++
 .../org/apache/sis/filter/ArithmeticFunction.java  |   8 +-
 .../org/apache/sis/filter/AssociationValue.java    |  22 +-
 .../org/apache/sis/filter/ConvertFunction.java     |  21 +-
 .../apache/sis/filter/DefaultFilterFactory.java    |  26 +-
 .../apache/sis/filter/InvalidXPathException.java   |  78 +++
 .../main/org/apache/sis/filter/LeafExpression.java |  15 +-
 .../main/org/apache/sis/filter/PropertyValue.java  |  72 +-
 .../main/org/apache/sis/filter/privy/XPath.java    |  18 +-
 .../apache/sis/filter/sqlmm/FunctionWithSRID.java  |  24 +-
 .../apache/sis/filter/sqlmm/SpatialFunction.java   |  36 +-
 .../sis/geometry/wrapper/jts/ConverterTo2D.java    | 227 +++++++
 .../sis/geometry/wrapper/jts/ShapeConverter.java   |  73 +-
 .../feature/builder/FeatureTypeBuilderTest.java    |  29 +
 .../geometry/wrapper/jts/ShapeConverterTest.java   |  40 +-
 .../org/apache/sis/metadata/sql/privy/Dialect.java |  65 +-
 .../apache/sis/metadata/sql/privy/Reflection.java  |   2 +
 .../apache/sis/metadata/sql/privy/Supports.java    |  15 +
 .../main/org/apache/sis/storage/landsat/Band.java  |  27 +-
 .../org/apache/sis/storage/landsat/BandGroup.java  |  15 +
 .../apache/sis/storage/landsat/LandsatStore.java   |  27 +-
 .../org/apache/sis/storage/sql/duckdb/DuckDB.java  |   2 +-
 .../sis/storage/sql/duckdb/package-info.java       |   4 +
 .../apache/sis/storage/sql/feature/Analyzer.java   |   7 +-
 .../org/apache/sis/storage/sql/feature/Column.java |  50 +-
 .../apache/sis/storage/sql/feature/Database.java   |  56 +-
 .../sis/storage/sql/feature/FeatureAdapter.java    |  20 +-
 .../sis/storage/sql/feature/FeatureIterator.java   |  31 +-
 .../sis/storage/sql/feature/FeatureStream.java     |  38 +-
 .../sis/storage/sql/feature/GeometryEncoding.java  |  93 ++-
 .../storage/sql/feature/GeometryTypeEncoding.java  |   2 +
 .../sis/storage/sql/feature/InfoStatements.java    | 124 +++-
 .../sis/storage/sql/feature/QueryAnalyzer.java     |   7 +-
 .../sis/storage/sql/feature/SelectionClause.java   |  10 +-
 .../storage/sql/feature/SelectionClauseWriter.java |  10 +-
 .../sis/storage/sql/feature/SpatialSchema.java     |   9 +-
 .../org/apache/sis/storage/sql/feature/Table.java  |  48 +-
 .../sis/storage/sql/feature/TableAnalyzer.java     |  10 +
 .../sis/storage/sql/feature/ValueGetter.java       |   2 +-
 .../apache/sis/storage/sql/postgis/Postgres.java   |   4 +-
 .../org/apache/sis/util/stream/DeferredStream.java |  10 +-
 .../org/apache/sis/storage/sql/SQLStoreTest.java   |  30 +-
 .../org/apache/sis/io/stream/ChannelDataInput.java |  15 +
 .../org/apache/sis/io/stream/HttpByteChannel.java  |  21 +
 .../main/org/apache/sis/storage/FeatureQuery.java  |  58 +-
 .../main/org/apache/sis/storage/FeatureSubset.java |   6 +-
 .../apache/sis/storage/base/FeatureProjection.java | 381 -----------
 .../sis/storage/base/TiledDeferredImage.java       |   1 +
 .../apache/sis/storage/base/TiledGridCoverage.java |  19 +-
 .../org/apache/sis/storage/internal/Resources.java |   5 +
 .../sis/storage/internal/Resources.properties      |   1 +
 .../sis/storage/internal/Resources_fr.properties   |   1 +
 .../main/org/apache/sis/storage/tiling/Tile.java   |  19 +
 .../sis/util/privy/UnmodifiableArrayList.java      |   3 +-
 .../apache/sis/storage/geoheif/FromImageIO.java    |  65 +-
 .../main/org/apache/sis/storage/geoheif/Image.java |  43 +-
 .../apache/sis/storage/geoheif/ImageResource.java  | 152 +++--
 .../sis/storage/geoheif/ResourceBuilder.java       |   6 +-
 .../sis/storage/geoheif/UncompressedImage.java     |  69 +-
 .../org/apache/sis/storage/isobmff/ByteRanges.java | 163 +++++
 .../org/apache/sis/storage/isobmff/ByteReader.java |  92 ---
 .../org/apache/sis/storage/isobmff/Reader.java     |  14 +-
 .../apache/sis/storage/isobmff/base/ItemData.java  |  32 +-
 .../sis/storage/isobmff/base/ItemLocation.java     |  96 +--
 .../sis/storage/shapefile/ShapefileStore.java      |  12 +-
 84 files changed, 3099 insertions(+), 1096 deletions(-)

diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
index 1ed310c507,5d27304400..0449cc2a29
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
@@@ -57,7 -62,7 +57,7 @@@ final class ExpressionOperation<V> exte
       * The expression to which to delegate the execution of this operation.
       */
      @SuppressWarnings("serial")                         // Not statically 
typed as serializable.
-     private final Function<? super AbstractFeature, ? extends V> expression;
 -    final Function<? super Feature, ? extends V> expression;
++    final Function<? super AbstractFeature, ? extends V> expression;
  
      /**
       * The type of result of evaluating the expression.
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
index 2dc4406318,05210b25e4..4df8b93098
--- 
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
@@@ -27,10 -27,17 +27,12 @@@ import org.apache.sis.util.Static
  import org.apache.sis.util.collection.WeakHashSet;
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.util.privy.Strings;
+ import org.apache.sis.filter.DefaultFilterFactory;
+ import org.apache.sis.filter.privy.XPath;
  import org.apache.sis.setup.GeometryLibrary;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.Operation;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.filter.Expression;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Expression;
  
  
  /**
@@@ -158,9 -152,9 +151,9 @@@ public final class FeatureOperations ex
       * @param  referent        the referenced attribute or feature 
association.
       * @return an operation which is an alias for the {@code referent} 
property.
       *
--     * @see Features#getLinkTarget(PropertyType)
++     * @see Features#getLinkTarget(AbstractIdentifiedType)
       */
 -    public static Operation link(final Map<String,?> identification, final 
PropertyType referent) {
 +    public static AbstractOperation link(final Map<String,?> identification, 
final AbstractIdentifiedType referent) {
          ArgumentChecks.ensureNonNull("referent", referent);
          return POOL.unique(new LinkOperation(identification, referent));
      }
@@@ -362,4 -348,44 +355,44 @@@
      {
          return function(identification, 
expression.toValueType(resultType.getValueClass()), resultType);
      }
+ 
+     /**
+      * Returns an expression for fetching the values of properties identified 
by the given type.
+      * The returned expression will be the first of the following choices 
which is applicable:
+      *
+      * <ul>
+      *   <li>If the property is an expression built by {@link #expression 
expression(…)}, then the
+      *       expression given to that method, or a derivative of that 
expression, is returned.</li>
+      *   <li>If the property {@linkplain Features#getLinkTarget is a link},
+      *       then a {@code ValueReference} fetching the link target is 
returned.</li>
+      *   <li>Otherwise, a {@linkplain DefaultFilterFactory.Features#property 
value reference}
+      *       is created for the name of the given property.</li>
+      * </ul>
+      *
+      * @param  property  the property for which to get an expression.
+      * @return an expression for fetching the values of the property 
identified by the given type.
+      * @since 1.5
+      */
 -    public static Expression<? super Feature, ?> expressionOf(final 
PropertyType property) {
++    public static Expression<? super AbstractFeature, ?> expressionOf(final 
AbstractIdentifiedType property) {
+         // Test final class first because it is fast.
+         if (property instanceof ExpressionOperation<?>) {
 -            final Function<? super Feature, ?> expression = 
((ExpressionOperation<?>) property).expression;
++            final Function<? super AbstractFeature, ?> expression = 
((ExpressionOperation<?>) property).expression;
+             if (expression instanceof Expression<?,?>) {
 -                return (Expression<? super Feature, ?>) expression;
++                return (Expression<? super AbstractFeature, ?>) expression;
+             }
+         }
+         String name;
+         final Class<?> type;
 -        if (property instanceof AttributeType<?>) {
 -            type = ((AttributeType<?>) property).getValueClass();
++        if (property instanceof DefaultAttributeType<?>) {
++            type = ((DefaultAttributeType<?>) property).getValueClass();
+             name = null;
+         } else {
+             type = Object.class;
+             name = Features.getLinkTarget(property).orElse(null);
+         }
+         if (name == null) {
+             name = property.getName().toString();
+         }
+         return 
DefaultFilterFactory.forFeatures().property(XPath.fromPropertyName(name), type);
+     }
  }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
index ea862f408f,00ad38f9d1..90d7b2d5e4
--- 
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
@@@ -260,6 -302,28 +261,28 @@@ public final class Features extends Sta
          return Optional.empty();
      }
  
+     /**
+      * If the given property is a link or a compound key, returns the name of 
the referenced properties.
 -     * This method is similar to {@link #getLinkTarget(PropertyType)}, except 
that it recognizes also
++     * This method is similar to {@link 
#getLinkTarget(AbstractIdentifiedType)}, except that it recognizes also
+      * the operations created by {@link FeatureOperations#compound 
FeatureOperations.compound(…)}.
+      *
+      * @param  property  the property to test, or {@code null} if none.
+      * @return the referenced property names if {@code property} is a link or 
a compound key,
+      *         or an empty list otherwise.
+      *
 -     * @see FeatureOperations#compound(Map, String, String, String, 
PropertyType...)
++     * @see FeatureOperations#compound(Map, String, String, String, 
AbstractIdentifiedType...)
+      *
+      * @since 1.5
+      */
 -    public static List<String> getLinkTargets(final PropertyType property) {
++    public static List<String> getLinkTargets(final AbstractIdentifiedType 
property) {
+         return getLinkTarget(property).map(List::of).orElseGet(() -> {
+             if (property instanceof StringJoinOperation) {
+                 return ((StringJoinOperation) property).getAttributeNames();
+             }
+             return List.of();
+         });
+     }
+ 
      /**
       * Ensures that all characteristics and property values in the given 
feature are valid.
       * An attribute is valid if it contains a number of values between the
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
index 75bcc06d7f,703c02faac..bb3d2431a0
--- 
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
@@@ -35,8 -36,21 +36,9 @@@ import org.apache.sis.util.privy.Collec
  import org.apache.sis.converter.SurjectiveConverter;
  import org.apache.sis.feature.privy.AttributeConvention;
  import org.apache.sis.feature.internal.Resources;
+ import org.apache.sis.util.privy.UnmodifiableArrayList;
  import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.InvalidPropertyValueException;
 -import org.opengis.feature.Operation;
 -import org.opengis.feature.Property;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.PropertyNotFoundException;
 -
  
  /**
   * An operation concatenating the string representations of the values of 
multiple properties.
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/PropertyTypeBuilder.java
index 7ce9c5ea4e,2c9405313e..d062c56da1
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/PropertyTypeBuilder.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/PropertyTypeBuilder.java
@@@ -18,9 -18,13 +18,10 @@@ package org.apache.sis.feature.builder
  
  import org.opengis.util.GenericName;
  import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.feature.internal.Resources;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.FeatureAssociationRole;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractIdentifiedType;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
index 9c1b95725a,03dbf4913a..cdf5e57109
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
@@@ -18,18 -18,20 +18,15 @@@ package org.apache.sis.feature.privy
  
  import java.util.Set;
  import org.apache.sis.math.FunctionProperty;
+ import org.apache.sis.util.UnconvertibleObjectException;
  import org.apache.sis.filter.Optimization;
  import org.apache.sis.filter.DefaultFilterFactory;
- import org.apache.sis.feature.builder.FeatureTypeBuilder;
- import org.apache.sis.feature.builder.PropertyTypeBuilder;
- import org.apache.sis.feature.builder.AttributeTypeBuilder;
  import org.apache.sis.filter.internal.Node;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.PropertyNotFoundException;
 -import org.opengis.filter.InvalidFilterValueException;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Expression;
- import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.pending.geoapi.filter.Literal;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
  
  
  /**
@@@ -72,21 -74,38 +69,38 @@@ public interface FeatureExpression<R,V
      }
  
      /**
-      * Provides the expected type of values produced by this expression when 
a feature of the given
-      * type is evaluated. The resulting type shall describe a "static" 
property, i.e. it can be a
-      * {@link org.apache.sis.feature.DefaultAttributeType} or a {@link 
org.apache.sis.feature.DefaultAssociationRole}
-      * but not an {@link org.apache.sis.feature.AbstractOperation}.
+      * Provides the expected type of values produced by this expression when 
a feature of a given type is evaluated.
+      * Except for the special case of links (described below), the resulting 
type shall describe a "static" property,
 -     * <i>i.e.</i> the type should be an {@link AttributeType} or a {@link 
org.opengis.feature.FeatureAssociationRole}
 -     * but not an {@link org.opengis.feature.Operation}. The value of the 
static property will be set to the result of
++     * <i>i.e.</i> the type should be an {@code AttributeType} or a {@code 
FeatureAssociationRole}
++     * but not an {@code Operation}. The value of the static property will be 
set to the result of
+      * evaluating the expression when instances of the {@code FeatureType} 
will be created.
 -     * This evaluation will be performed by {@link 
FeatureProjection#apply(Feature)}.
++     * This evaluation will be performed by {@link 
FeatureProjection#apply(AbstractFeature)}.
       *
-      * <p>If this method returns an instance of {@link AttributeTypeBuilder}, 
then its parameterized
-      * type should be the same {@code <V>} than this {@code 
FeatureExpression}.</p>
+      * <h4>Implementation guideline</h4>
+      * Implementations should declare the property by invoking some of the 
following methods:
       *
-      * @param  valueType  the type of features to be evaluated by the given 
expression.
-      * @param  addTo      where to add the type of properties evaluated by 
this expression.
-      * @return builder of the added property, or {@code null} if this method 
cannot add a property.
-      * @throws IllegalArgumentException if this method can operate only on 
some feature types
-      *         and the given type is not one of them.
+      * <ul>
+      *   <li>{@link FeatureProjectionBuilder#source()} for the source of the 
{@link PropertyType} in next point.</li>
 -     *   <li>{@link FeatureProjectionBuilder#addSourceProperty(PropertyType, 
boolean)}</li>
++     *   <li>{@link 
FeatureProjectionBuilder#addSourceProperty(AbstractIdentifiedType, 
boolean)}</li>
+      *   <li>{@link 
FeatureProjectionBuilder#addComputedProperty(PropertyTypeBuilder, boolean)}</li>
+      * </ul>
+      *
+      * Inherited methods such as {@link 
FeatureProjectionBuilder#addAttribute(Class)} can also be invoked,
+      * but callers will be responsible for providing the value of the 
properties added by those methods.
 -     * These values will not be provided by {@link 
FeatureProjection#apply(Feature)}.
++     * These values will not be provided by {@link 
FeatureProjection#apply(AbstractFeature)}.
+      *
+      * <h4>Operations</h4>
+      * If the property is a link to another property, such as {@code 
"sis:identifier"} or {@code "sis:geometry"},
+      * then adding this property may require the addition of dependencies. 
These dependencies will be detected by
+      * {@link FeatureProjectionBuilder}, which may generate an intermediate 
{@code FeatureType}.
+      *
+      * @param  addTo  where to add the type of the property evaluated by this 
expression.
+      * @return handler of the added property, or {@code null} if the property 
cannot be added.
 -     * @throws InvalidFilterValueException if this expression is invalid for 
the requested operation.
 -     * @throws PropertyNotFoundException if the property was not found in 
{@code addTo.source()}.
++     * @throws IllegalArgumentException if this expression is invalid for the 
requested operation.
++     * @throws IllegalArgumentException if the property was not found in 
{@code addTo.source()}.
+      * @throws UnconvertibleObjectException if the property default value 
cannot be converted to the expected type.
       */
-     PropertyTypeBuilder expectedType(DefaultFeatureType valueType, 
FeatureTypeBuilder addTo);
+     FeatureProjectionBuilder.Item expectedType(FeatureProjectionBuilder 
addTo);
  
      /**
       * Tries to cast or convert the given expression to a {@link 
FeatureExpression}.
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjection.java
index 0000000000,b263c1e4dc..9796665544
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjection.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjection.java
@@@ -1,0 -1,341 +1,341 @@@
+ /*
+  * Licensed to the Apache Software Foundation (ASF) under one or more
+  * contributor license agreements.  See the NOTICE file distributed with
+  * this work for additional information regarding copyright ownership.
+  * The ASF licenses this file to You under the Apache License, Version 2.0
+  * (the "License"); you may not use this file except in compliance with
+  * the License.  You may obtain a copy of the License at
+  *
+  *     http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS,
+  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  * See the License for the specific language governing permissions and
+  * limitations under the License.
+  */
+ package org.apache.sis.feature.privy;
+ 
+ import java.util.List;
+ import java.util.Set;
+ import java.util.Map;
+ import java.util.LinkedHashMap;
+ import java.util.Optional;
+ import java.util.function.UnaryOperator;
+ import org.apache.sis.util.Debug;
+ import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.resources.Vocabulary;
+ import org.apache.sis.util.privy.UnmodifiableArrayList;
+ import org.apache.sis.filter.privy.ListingPropertyVisitor;
+ import org.apache.sis.io.TableAppender;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
++// Specific to the main branch:
++import org.apache.sis.filter.Expression;
++import org.apache.sis.feature.AbstractFeature;
++import org.apache.sis.feature.DefaultFeatureType;
++import org.apache.sis.pending.geoapi.filter.ValueReference;
+ 
+ 
+ /**
+  * A function applying projections (with "projection" in the <abbr>SQL</abbr> 
database sense) of features.
+  * Given full feature instances in input, the function returns feature 
instances containing only a subset
+  * of the properties. The property values may also be different if they are 
computed by the expressions.
+  *
+  * @author Guilhem Legal (Geomatys)
+  * @author Martin Desruisseaux (Geomatys)
+  */
 -public final class FeatureProjection implements UnaryOperator<Feature> {
++public final class FeatureProjection implements 
UnaryOperator<AbstractFeature> {
+     /**
+      * The type of features with the properties explicitly requested by the 
user.
+      * The property names may differ from the properties of the {@link 
FeatureProjectionBuilder#source() source}
+      * features if aliases were specified by calls to {@link 
FeatureProjectionBuilder.Item#setName(GenericName)}.
+      */
 -    public final FeatureType typeRequested;
++    public final DefaultFeatureType typeRequested;
+ 
+     /**
+      * The requested type augmented with dependencies required for the 
execution of operations such as links.
+      * If there is no need for additional properties, then this value is the 
same as {@link #typeRequested}.
+      * The property names are the same as {@link #typeRequested} (i.e., may 
be aliases).
+      */
 -    public final FeatureType typeWithDependencies;
++    public final DefaultFeatureType typeWithDependencies;
+ 
+     /**
+      * Names of the properties to be stored in the feature instances created 
by this {@code FeatureProjection}.
+      * Properties that are computed on-the-fly from other properties are not 
included in this array. Properties
+      * that are present in {@link #typeRequested} but not in the source 
features are not in this array neither.
+      */
+     private final String[] propertiesToCopy;
+ 
+     /**
+      * Expressions to apply on the source feature for fetching the property 
values of the projected feature.
+      * This array has the same length as {@link #propertiesToCopy} and each 
expression is associated to the
+      * property at the same index.
+      */
 -    private final Expression<? super Feature, ?>[] expressions;
++    private final Expression<? super AbstractFeature, ?>[] expressions;
+ 
+     /**
 -     * Whether the {@link #apply(Feature)} method shall create instances of 
{@link #typeWithDependencies}.
 -     * If {@code false}, then the instances given to the {@link 
#apply(Feature)} method will be assumed
++     * Whether the {@link #apply(AbstractFeature)} method shall create 
instances of {@link #typeWithDependencies}.
++     * If {@code false}, then the instances given to the {@link 
#apply(AbstractFeature)} method will be assumed
+      * to be already instances of {@link #typeWithDependencies} and will be 
modified in-place.
+      */
+     private final boolean createInstance;
+ 
+     /**
+      * Creates a new projection with the given properties specified by a 
builder.
 -     * The {@link #apply(Feature)} method will copy the properties of the 
given
++     * The {@link #apply(AbstractFeature)} method will copy the properties of 
the given
+      * features into new instances of {@link #typeWithDependencies}.
+      *
+      * @param  typeRequested  the type of projected features.
+      * @param  projection     descriptions of the properties to keep in the 
projected features.
+      */
 -    FeatureProjection(final FeatureType typeRequested, final FeatureType 
typeWithDependencies,
++    FeatureProjection(final DefaultFeatureType typeRequested, final 
DefaultFeatureType typeWithDependencies,
+                       final List<FeatureProjectionBuilder.Item> projection)
+     {
+         this.createInstance       = true;
+         this.typeRequested        = typeRequested;
+         this.typeWithDependencies = typeWithDependencies;
+         int storedCount = 0;
+ 
+         // Expressions to apply on the source feature for fetching the 
property values of the projected feature.
+         @SuppressWarnings({"LocalVariableHidesMemberVariable", "unchecked", 
"rawtypes"})
 -        final Expression<? super Feature,?>[] expressions = new 
Expression[projection.size()];
++        final Expression<? super AbstractFeature,?>[] expressions = new 
Expression[projection.size()];
+ 
+         // Names of the properties to be stored in the attributes of the 
target features.
+         @SuppressWarnings("LocalVariableHidesMemberVariable")
+         final String[] propertiesToCopy = new String[expressions.length];
+ 
+         for (final FeatureProjectionBuilder.Item item : projection) {
+             final var expression = item.attributeValueGetter();
+             if (expression != null) {
+                 expressions[storedCount] = expression;
+                 propertiesToCopy[storedCount++] = item.getName();
+             }
+         }
+         this.propertiesToCopy = ArraysExt.resize(propertiesToCopy, 
storedCount);
+         this.expressions      = ArraysExt.resize(expressions, storedCount);
+     }
+ 
+     /**
+      * Creates a new projection with a subset of the properties of another 
projection.
+      * This constructor is invoked when the caller handles itself some of the 
properties.
+      *
+      * <h4>behavioral change</h4>
+      * Projections created by this constructor assumes that the feature 
instances given to the
 -     * {@link #apply(Feature)} method are already instances of {@link 
#typeWithDependencies}
++     * {@link #apply(AbstractFeature)} method are already instances of {@link 
#typeWithDependencies}
+      * and can be modified (if needed) in place. This constructor is designed 
for cases where
+      * the caller does itself a part of the {@code FeatureProjection} work.
+      *
+      * @param  parent     the projection from which to inherit the types and 
expressions.
+      * @param  remaining  index of the properties that still need to be 
copied after the caller did its processing.
+      *
+      * @see #afterPreprocessing(int[])
+      */
+     @SuppressWarnings({"rawtypes", "unchecked"})
+     private FeatureProjection(final FeatureProjection parent, final int[] 
remaining) {
+         createInstance       = false;
+         typeRequested        = parent.typeRequested;
+         typeWithDependencies = parent.typeWithDependencies;
+         expressions          = new Expression[remaining.length];
+         propertiesToCopy     = new String[remaining.length];
+         for (int i=0; i<remaining.length; i++) {
+             final int index = remaining[i];
+             propertiesToCopy[i] = parent.propertiesToCopy[index];
+             expressions[i] = parent.expressions[index];
+         }
+     }
+ 
+     /**
+      * Returns a variant of this projection where the caller has created the 
target feature instance itself.
+      * The callers is may have set some property values itself, and the 
{@code remaining} argument gives the
+      * indexes of the properties that still need to be copied after caller's 
processing.
+      *
+      * @param  remaining  index of the properties that still need to be 
copied after the caller did its processing.
+      * @return a variant of this projection which only completes the 
projection done by the caller,
+      *         or {@code null} if there is nothing left to complete.
+      */
+     public FeatureProjection afterPreprocessing(final int[] remaining) {
+         if (remaining.length == 0 && typeRequested == typeWithDependencies) {
+             return null;
+         }
+         return new FeatureProjection(this, remaining);
+     }
+ 
+     /**
+      * Returns the names of all stored properties. This list may be shorter 
than the list of properties of the
+      * {@linkplain #typeRequested requested feature type} if some feature 
properties are computed on-the-fly,
+      * or if the target feature contains some new properties that are not in 
the source.
+      *
+      * @return the name of all stored properties.
+      */
+     public final List<String> propertiesToCopy() {
+         return UnmodifiableArrayList.wrap(propertiesToCopy);
+     }
+ 
+     /**
+      * Returns the path to the value (in source features) of the property at 
the given index.
+      * The argument corresponds to an index in the list returned by {@link 
#propertiesToCopy()}.
+      * The return value is often the same name as {@code 
propertiesToCopy().get(index)} but may
+      * differ if the user has specified aliases or if two properties have the 
same default name.
+      *
+      * @param  index  index of the stored property for which to get the name 
in the source feature.
+      * @return path in the source features, or empty if the property is not a 
{@link ValueReference}.
+      */
+     public Optional<String> xpath(final int index) {
 -        final Expression<? super Feature, ?> expression = expressions[index];
++        final Expression<? super AbstractFeature, ?> expression = 
expressions[index];
+         if (expression instanceof ValueReference<?,?>) {
+             return Optional.of(((ValueReference<?,?>) expression).getXPath());
+         }
+         return Optional.empty();
+     }
+ 
+     /**
+      * Returns all dependencies used, directly or indirectly, by all 
expressions used in this projection.
+      * The set includes transitive dependencies (expressions with operands 
that are other expressions).
+      * The elements are XPaths.
+      *
+      * @return all dependencies (including transitive dependencies) as XPaths.
+      */
+     public Set<String> dependencies() {
+         Set<String> references = null;
+         for (var expression : expressions) {
+             references = ListingPropertyVisitor.xpaths(expression, 
references);
+         }
+         return (references != null) ? references : Set.of();
+     }
+ 
+     /**
+      * Derives a new projected feature instance from the given source.
+      * The feature type of the returned feature instance will be be {@link 
#typeRequested}.
+      * This method performs the following steps:
+      *
+      * <ol class="verbose">
+      *   <li>If this projection was created by {@link 
#afterPreprocessing(int[])}, then the given feature
+      *     shall be an instances of {@link #typeWithDependencies} and may be 
modified in-place. Otherwise,
+      *     this method creates a new instance of {@link 
#typeWithDependencies}.</li>
+      *   <li>This method executes all expressions for fetching values from 
{@code source}
+      *     and stores the results in the feature instance of above step.</li>
+      *   <li>If {@link #typeWithDependencies} is different than {@link 
#typeRequested}, then the feature
+      *     of above step is wrapped in a view which hides the undesired 
properties.</li>
+      * </ol>
+      *
+      * @param  source the source feature instance.
+      * @return the "projected" (<abbr>SQL</abbr> database sense) feature 
instance.
+      */
+     @Override
 -    public Feature apply(final Feature source) {
++    public AbstractFeature apply(final AbstractFeature source) {
+         var feature = createInstance ? typeWithDependencies.newInstance() : 
source;
+         for (int i=0; i < expressions.length; i++) {
+             feature.setPropertyValue(propertiesToCopy[i], 
expressions[i].apply(source));
+         }
+         if (typeRequested != typeWithDependencies) {
+             feature = new FeatureView(typeRequested, feature);
+         }
+         return feature;
+     }
+ 
+     /**
+      * Returns a string representation of this projection for debugging 
purposes.
+      * The current implementation formats a table with all properties, 
including
+      * dependencies, and a column saying whether the property is an operation,
+      * is stored or whether there is an error.
+      *
+      * @return a string representation.
+      */
+     @Override
+     public String toString() {
+         return Row.toString(this, propertiesToCopy);
+     }
+ 
+     /**
+      * Helper class for the implementation of {@link 
FeatureProjection#toString()}.
+      * Each instance represents a row in the table to be formatted..
+      * This is used for debugging purposes only.
+      */
+     @Debug
+     private static final class Row {
+         /**
+          * Returns a string representation of the {@link FeatureProjection} 
having the given values.
+          * Having this method in a separated class reduces the amount of 
classes loading, since this
+          * {@code Row} class is rarely needed in production environment.
+          *
+          * @param  projection        the projection for which to format a 
string representation.
+          * @param  propertiesToCopy  value of {@link 
FeatureProjection#propertiesToCopy}.
+          * @return the string representation of the given projection.
+          */
+         static String toString(final FeatureProjection projection, final 
String[] propertiesToCopy) {
+             final var rowByName = new LinkedHashMap<String,Row>();
+             if (projection.typeWithDependencies != projection.typeRequested) {
+                 addAll(rowByName, projection.typeWithDependencies, 
"dependency");
+             }
+             addAll(rowByName, projection.typeRequested, "operation");   // 
Overwrite above dependencies.
+             for (int i=0; i < propertiesToCopy.length; i++) {
+                 String name = propertiesToCopy[i];
+                 String value;
+                 try {
+                     name = 
projection.typeWithDependencies.getProperty(name).getName().toString();
+                     value = "stored";
+                 } catch (RuntimeException e) {
+                     value = e.toString();
+                 }
+                 Row row   = rowByName.computeIfAbsent(name, Row::new);
+                 row.type  = value;
+                 row.xpath = projection.xpath(i).orElse("");
+             }
+             final var words = Vocabulary.forLocale(null);
+             final var table = new TableAppender(" │ ");
+             table.setMultiLinesCells(true);
+             table.appendHorizontalSeparator();
+             
table.append(words.getString(Vocabulary.Keys.Property)).nextColumn();
+             table.append(words.getString(Vocabulary.Keys.Type)).nextColumn();
+             table.append("XPath").nextLine();
+             table.appendHorizontalSeparator();
+             for (final Row row : rowByName.values()) {
+                 table.append(row.property).nextColumn();
+                 table.append(row.type).nextColumn();
+                 table.append(row.xpath).nextLine();
+             }
+             table.appendHorizontalSeparator();
+             return table.toString();
+         }
+ 
+         /**
+          * Adds all properties of the given feature type into the specified 
map.
+          * For each property, {@link #type} is overwritten with the {@code 
type} argument value.
+          *
+          * @param rowByName    where to add the properties.
+          * @param featureType  the type from which to get the list of 
properties.
+          * @param type         the "stored", "operation" or "dependency" 
value to assign to {@link #type}.
+          */
 -        private static void addAll(final Map<String,Row> rowByName, final 
FeatureType featureType, final String type) {
++        private static void addAll(final Map<String,Row> rowByName, final 
DefaultFeatureType featureType, final String type) {
+             for (final var property : featureType.getProperties(true)) {
+                 Row row = 
rowByName.computeIfAbsent(property.getName().toString(), Row::new);
+                 row.type = type;
+             }
+         }
+ 
+         /**
+          * Creates a row with no value.
+          *
+          * @param  name  number under which the property is stored.
+          */
+         private Row(final String name) {
+             property = name;
+             xpath = "";
+         }
+ 
+         /**
+          * Name of the property in the projected feature type.
+          */
+         private final String property;
+ 
+         /**
+          * Path to the property value in the source feature.
+          */
+         private String xpath;
+ 
+         /**
+          * Type: stored, operation or dependency.
+          */
+         private String type;
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
index 0000000000,2f9c7bd6df..d3d6af82db
mode 000000,100644..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
@@@ -1,0 -1,741 +1,739 @@@
+ /*
+  * Licensed to the Apache Software Foundation (ASF) under one or more
+  * contributor license agreements.  See the NOTICE file distributed with
+  * this work for additional information regarding copyright ownership.
+  * The ASF licenses this file to You under the Apache License, Version 2.0
+  * (the "License"); you may not use this file except in compliance with
+  * the License.  You may obtain a copy of the License at
+  *
+  *     http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS,
+  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  * See the License for the specific language governing permissions and
+  * limitations under the License.
+  */
+ package org.apache.sis.feature.privy;
+ 
+ import java.util.Map;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.ArrayList;
+ import java.util.Iterator;
+ import java.util.Locale;
+ import java.util.Objects;
+ import java.util.Optional;
+ import java.util.function.UnaryOperator;
+ import org.opengis.util.GenericName;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.apache.sis.feature.Features;
+ import org.apache.sis.feature.AbstractOperation;
+ import org.apache.sis.feature.FeatureOperations;
+ 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.util.ArgumentCheckByAssertion;
+ import org.apache.sis.util.UnconvertibleObjectException;
+ import org.apache.sis.util.privy.Strings;
+ import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.resources.Vocabulary;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.Operation;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
++// Specific to the main branch:
++import org.apache.sis.filter.Expression;
++import org.apache.sis.feature.AbstractFeature;
++import org.apache.sis.feature.DefaultFeatureType;
++import org.apache.sis.feature.DefaultAttributeType;
++import org.apache.sis.feature.AbstractIdentifiedType;
++import org.apache.sis.pending.geoapi.filter.ValueReference;
+ 
+ 
+ /**
+  * A builder for deriving a feature type containing a subset of the 
properties of another type.
+  * The other type is called the {@linkplain #source() source} feature type. 
The properties that
+  * are retained may have different names than the property names of the 
source feature type.
+  * If a property is a link such as {@code sis:identifier} or {@code 
sis:geometry},
+  * this class keeps trace of the dependencies required for recreating the 
link.
+  *
+  * <p>Properties that are copied from the source feature type are declared by 
calls to
 - * {@link #addSourceProperty(PropertyType, boolean)} and related methods 
defined in this class.
++ * {@link #addSourceProperty(AbstractIdentifiedType, boolean)} and related 
methods defined in this class.
+  * The methods inherited from the parent class can also be invoked,
+  * but they will receive no special treatment.</p>
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ public final class FeatureProjectionBuilder extends FeatureTypeBuilder {
+     /**
+      * The type of features that provide the values to store in the projected 
features.
+      * The value of this field does not change, except when following a XPath 
such as {@code "a/b/c"}.
+      *
+      * @see #source()
+      */
 -    private FeatureType source;
++    private DefaultFeatureType source;
+ 
+     /**
+      * Whether the source is a dependency of the feature type given to the 
constructor.
+      * This flag become {@code true} when following a XPath of the form 
{@code "a/b/c"}.
+      * In such case, {@link #source} may be temporarily set to the tip {@code 
"c"} type.
+      *
 -     * @see #using(FeatureType, FeatureExpression)
++     * @see #using(DefaultFeatureType, FeatureExpression)
+      */
+     private boolean sourceIsDependency;
+ 
+     /**
+      * The properties to inherit from the {@linkplain #source} feature type 
by explicit user's request.
+      * The property types and the property names are not necessarily the same 
as in the source feature.
+      * For example, an operation may be replaced by an attribute which will 
store the operation result.
+      *
+      * <h4>Implementation note</h4>
+      * This collection cannot be a {@link java.util.Map} with names of the 
source properties as keys,
+      * because the list may contain more than one item with the same {@link 
Item#sourceName} value.
+      * This collision happens if some items are the results of XPath 
evaluations such as {@code "a/b/c"}.
+      * This is the reason why properties can be renamed before to be stored 
in the projected features.
+      */
+     private final List<Item> requested;
+ 
+     /**
+      * Names that are actually used, or may be used, in the projected feature 
type.
+      * For each name, the associated value is the item that is explicitly 
using that name.
+      * A {@code null} value means that the name is not used, but is 
nevertheless reserved
+      * because potentially ambiguous. This information is used for avoiding 
name collisions
+      * in automatically generated names.
+      *
+      * <p>Note that the keys are not necessarily the values of {@link 
Item#sourceName}.
+      * Keys are rather the values of {@code Item.builder.getName()}, except 
that the
+      * latter may not be valid before {@link Item#validateName()} is 
invoked.</p>
+      *
+      * @see #reserve(GenericName, Item)
+      */
+     private final Map<GenericName, Item> reservedNames;
+ 
+     /**
+      * Whether at least one item is modified compared to the original 
property in the source feature type.
+      * A modified item may be an item with a name different than the property 
in {@linkplain #source}.
+      * If {@code true}, then the projection cannot be an {@linkplain 
#isIdentity() identity} operation.
+      * Result of operations such as links may also need to be fetched in 
advance,
+      * because operations cannot be executed anymore after the name of a 
dependency changed.
+      *
+      * @see Item#setName(GenericName)
+      * @see #isIdentity()
+      */
+     private boolean hasModifiedProperties;
+ 
+     /**
+      * Sequential number for generating default names of unnamed properties. 
This is used when the
+      * name inherited from the {@linkplain #source} feature is unknown or 
collides with another name.
+      */
+     private int unnamedNumber;
+ 
+     /**
+      * Names of {@linkplain #source} properties that are dependencies found 
in operations.
+      * The most common cases are the targets of {@code "sis:identifier"} and 
{@code "sis:geometry"} links.
+      * Values are the items having this dependency. Many items may share the 
same dependency.
+      *
+      * <p>At first, this map is populated without checking if the properties 
requested by the user contains
+      * those dependencies. After all user's requested properties have been 
declared to this builder, values
+      * are filtered for identifying which dependencies need to be added as 
implicit properties.</p>
+      */
+     private final Map<String, List<Item>> dependencies;
+ 
+     /**
+      * Creates a new builder instance using the default factories.
+      *
+      * @todo provides a way to specify the factories used by the data store.
+      *
+      * @param source  the type from which to take the properties to keep in 
the projected feature.
+      * @param locale  the locale to use for formatting error messages, or 
{@code null} for the default locale.
+      */
 -    public FeatureProjectionBuilder(final FeatureType source, final Locale 
locale) {
++    public FeatureProjectionBuilder(final DefaultFeatureType source, final 
Locale locale) {
+         super(null, null, locale);
+         this.source   = Objects.requireNonNull(source);
+         requested     = new ArrayList<>();
+         dependencies  = new HashMap<>();
+         reservedNames = new HashMap<>();
+     }
+ 
+     /**
+      * Returns the type of features that provide the values to store in the 
projected features.
+      * This is the type given at construction time, except when following a 
XPath such as
+      * {@code "a/b/c"} in which case it may temporarily be the leaf {@code 
"c"} type.
+      *
+      * @return the current source of properties (never {@code null}).
+      */
 -    public FeatureType source() {
++    public DefaultFeatureType source() {
+         return source;
+     }
+ 
+     /**
+      * Returns the expected type of the given expression using the given 
feature type as the source.
+      * This method temporarily sets the {@linkplain #source() source} to the 
given {@code childType},
+      * then returns the value of {@code expression.expectedType(this)}.
+      * This is used for the last expression in a XPath such as {@code 
"a/b/c"}.
+      *
+      * @param  childType   the feature type to use.
+      * @param  expression  the expression from which to get the expected type.
+      * @return the expected type, or {@code null}.
+      *
+      * @see FeatureExpression#expectedType(FeatureProjectionBuilder)
+      */
 -    public Item using(final FeatureType childType, final 
FeatureExpression<?,?> expression) {
 -        final FeatureType previous = source;
++    public Item using(final DefaultFeatureType childType, final 
FeatureExpression<?,?> expression) {
++        final DefaultFeatureType previous = source;
+         final boolean status = sourceIsDependency;
+         try {
+             sourceIsDependency = true;
+             source = Objects.requireNonNull(childType);
+             return expression.expectedType(this);
+         } finally {
+             source = previous;
+             sourceIsDependency = status;
+         }
+     }
+ 
+     /**
+      * Adds the given property, replacing operation by an attribute storing 
the operation result.
+      * 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.
+      *
+      * @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) {
 -        if (property instanceof Operation) {
++    private PropertyTypeBuilder addPropertyResult(AbstractIdentifiedType 
property, final List<String> deferred) {
++        if (property instanceof AbstractOperation) {
+             final GenericName name = property.getName();
+             do {
+                 if (deferred != null) {
+                     if (property instanceof AbstractOperation) {
+                         deferred.addAll(((AbstractOperation) 
property).getDependencies());
+                     } else {
+                         /*
+                          * Cannot resolve dependencies. Current 
implementation assumes that there is no dependency.
+                          * Note: we could conservatively add all properties 
as dependencies, but this is difficult
+                          * to implement efficiently.
+                          */
+                     }
+                 }
 -                final IdentifiedType result = ((Operation) 
property).getResult();
 -                if (result != property && result instanceof PropertyType) {
 -                    property = (PropertyType) result;
 -                } else if (result instanceof FeatureType) {
 -                    return addAssociation((FeatureType) result).setName(name);
++                final AbstractIdentifiedType result = ((AbstractOperation) 
property).getResult();
++                if (result != property && result instanceof 
AbstractIdentifiedType) {
++                    property = result;
++                } else if (result instanceof DefaultFeatureType) {
++                    return addAssociation((DefaultFeatureType) 
result).setName(name);
+                 } else {
+                     return null;
+                 }
 -            } while (property instanceof Operation);
++            } while (property instanceof AbstractOperation);
+             return addProperty(property).setName(name);
+         }
+         return addProperty(property);
+     }
+ 
+     /**
+      * Adds a property from the source feature type. The given property 
should be the result of a call to
+      * {@code source().getProperty(sourceName)}. The call to {@code 
getProperty(…)} is left to the caller
+      * because some callers need to wrap that call in a {@code try} block.
+      *
+      * @param  property  the property type, usually as one of the properties 
of {@link #source()}.
+      * @param  named     whether the {@code property} name can be used as a 
default name.
+      * @return handler for the given item, or {@code null} if the given 
property cannot be resolved.
+      */
 -    public Item addSourceProperty(final PropertyType property, final boolean 
named) {
++    public Item addSourceProperty(final AbstractIdentifiedType property, 
final boolean named) {
+         if (property == null) {
+             return null;
+         }
+         final PropertyTypeBuilder builder;
+         List<String> deferred;
+         if (sourceIsDependency) {
+             /*
+              * Adding a property which is not defined in the feature type 
specified at construction time,
+              * but which is defined at the tip of some XPath such as "a/b/c". 
This is not the same thing
+              * as adding an association. This is rather adding a subset of an 
association. We do not add
+              * dependency information because the dependencies are not 
directly in the source feature.
+              */
+             reserve(property.getName(), null);
+             deferred = new ArrayList<>();
+             builder = addPropertyResult(property, deferred);
+         } else {
+             /*
+              * For link 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.
+              */
+             deferred = Features.getLinkTargets(property);
+             if (deferred.isEmpty()) {
+                 deferred = new ArrayList<>();
+                 builder = addPropertyResult(property, deferred);
+             } else {
+                 builder = addProperty(property);
+             }
+         }
+         final var item = new Item(named ? property.getName() : null, builder);
+         requested.add(item);
+         for (String dependency : deferred) {
+             dependencies.computeIfAbsent(dependency, (key) -> new 
ArrayList<>(2)).add(item);
+         }
+         return item;
+     }
+ 
+     /**
+      * Adds a property created by the caller rather than extracted from the 
source feature.
+      * The given builder should have been created by a method of the {@link 
FeatureTypeBuilder} parent class.
+      * The name of the builder is usually not the name of a property in the 
{@linkplain #source() source} feature.
+      *
+      * <h4>Assertions</h4>
+      * This method verifies that the given builder is a member of the 
{@linkplain #properties() properties} collection.
+      * It also verifies that no {@link Item} have been created for that 
builder yet.
+      * For performance reasons, those verifications are performed only if 
assertions are enabled.
+      *
+      * @param  builder  builder for the computed property, or {@code null}.
+      * @param  named    whether the {@code builder} name can be used as a 
default name.
+      * @return handler for the given item, or {@code null} if the given 
builder was null.
+      */
+     @ArgumentCheckByAssertion
+     public Item addComputedProperty(final PropertyTypeBuilder builder, final 
boolean named) {
+         if (builder == null) {
+             return null;
+         }
+         assert properties().contains(builder) : builder;
+         assert requested.stream().noneMatch((item) ->item.builder() == 
builder) : builder;
+         final var item = new Item(named ? builder.getName() : null, builder);
+         requested.add(item);
+         return item;
+     }
+ 
+     /**
+      * Handler for a property inherited from the source feature type. The 
property is initially unnamed.
+      * A name can be specified explicitly after construction by a call to 
{@link #setName(GenericName)}.
+      * If no name is specified, the default name will be the same as in the 
source feature type if that
+      * name is available, or a default name otherwise.
+      */
+     public final class Item {
+         /**
+          * The name that the property had in the {@linkplain #source() 
source} feature, or {@code null}.
+          * The property built by the {@linkplain #builder} will often have 
the same name, but not always.
+          */
+         final GenericName sourceName;
+ 
+         /**
+          * The builder for configuring the property.
+          */
+         private PropertyTypeBuilder builder;
+ 
+         /**
+          * Whether this item got an explicit name. The specified name may be
+          * identical to the name in the {@linkplain #source() source} feature.
+          */
+         private boolean isNamed;
+ 
+         /**
+          * Whether to keep the current name if it is available. This is set 
to {@code true} when user did not
+          * specified explicitly a name, but keeping the name of the source 
property would be a natural choice.
+          * However, before to use that name, we need to wait and see if that 
name will be explicitly used for
+          * another property.
+          */
+         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.
+          *
+          * @see #replaceIfMissingDependency()
+          */
+         private boolean hasMissingDependency;
+ 
+         /**
+          * Expression for evaluating the attribute value from a source 
feature instance, or {@code null} if none.
+          * This field should be non-null only if the value will be stored in 
an attribute. If the property is an
+          * operation, then this field should be null (this is not the 
expression of the operation).
+          *
+          * @see #attributeValueGetter()
+          */
 -        private Expression<? super Feature, ?> attributeValueGetter;
++        private Expression<? super AbstractFeature, ?> attributeValueGetter;
+ 
+         /**
+          * Creates a new handle for the property created by the given builder.
+          *
+          * @param  sourceName  the property name in the {@linkplain #source() 
source} feature, or {@code null}.
+          * @param  builder     the builder for configuring the property.
+          */
+         private Item(final GenericName sourceName, final PropertyTypeBuilder 
builder) {
+             this.sourceName = sourceName;
+             this.builder = builder;
+         }
+ 
+         /**
+          * Returns a string representation for debugging purposes.
+          */
+         @Override
+         public String toString() {
+             return Strings.toString(getClass(),
+                     "sourceName", (sourceName != null) ? 
sourceName.toString() : null,
+                     "targetName", isNamed ? getName() : null,
+                     "valueClass", (builder instanceof 
AttributeTypeBuilder<?>) ? ((AttributeTypeBuilder<?>) builder).getValueClass() 
: null,
+                     null, hasMissingDependency ? "hasMissingDependency" : 
null);
+         }
+ 
+         /**
+          * Returns the property type builder wrapped by this item.
+          * The following operations are allowed on the returned builder:
+          *
+          * <ul>
+          *   <li>Set the cardinality (minimum and maximum occurrences).</li>
+          *   <li>Build the {@code PropertyType}.</li>
+          * </ul>
+          *
+          * The following operations should <em>not</em> be executed on the 
returned builder.
+          * Use the dedicated methods in this class instead:
+          *
+          * <ul>
+          *   <li>Set the name: use {@link #setName(GenericName)}.</li>
+          *   <li>Set the value class: use {@link 
#replaceValueClass(UnaryOperator)}.</li>
+          * </ul>
+          *
+          * @return the property type builder wrapped by this item.
+          */
+         public PropertyTypeBuilder builder() {
+             hasModifiedProperties = true;       // Conservative because the 
caller may do anything on the builder.
+             return builder;
+         }
+ 
+         /**
+          * Replaces this property by a stored attribute if at least one 
dependency is not in the list of properties
+          * requested by the user. This method should be invoked only for 
preparing the user requested feature type.
+          * This method should not be invoked for preparing the feature type 
with dependencies, because the latter
+          * should contain the missing dependencies.
+          */
+         private void replaceIfMissingDependency() {
+             if (hasMissingDependency) {
+                 hasMissingDependency = false;
+                 hasModifiedProperties = true;
+                 final var old = builder;
+                 builder = addPropertyResult(old.build(), null);   // 
`old.build()` returns the existing operation.
+                 old.replaceBy(builder);
+             }
+         }
+ 
+         /**
+          * Sets the class of attribute values. If the builder is an instance 
of {@link AttributeTypeBuilder}
+          * and if {@code type.apply(valueClass)} returns a non-null value 
({@code valueClass} is the current
+          * class of attribute values), then this method sets the new 
attribute value class to the specified
+          * type and returns {@code true}. Otherwise, this method returns 
{@code false}.
+          *
+          * @param  type  a converter from current class to the new class of 
attribute values.
+          * @return whether the value class has been set to the value returned 
by {@code type}.
+          * @throws UnconvertibleObjectException if the default value cannot 
be converted to the given type.
+          */
+         public boolean replaceValueClass(final UnaryOperator<Class<?>> type) {
+             if (builder instanceof AttributeTypeBuilder<?>) {
+                 final var ab = (AttributeTypeBuilder<?>) builder;
+                 final Class<?> r = type.apply(ab.getValueClass());
+                 if (r != null) {
+                     if (builder != (builder = ab.setValueClass(r))) {
+                         hasModifiedProperties = true;
+                     }
+                     return true;
+                 }
+             } else if (builder instanceof AssociationRoleBuilder) {
+                 // We do not yet have a special case for this one.
+             } else {
+                 final var property = builder.build();
 -                if (property instanceof Operation) {
++                if (property instanceof AbstractOperation) {
+                     /*
+                      * Less common case where the caller wants to change the 
type of an operation.
+                      * We cannot change the type of an operation (unless we 
replace the operation
+                      * by a stored attribute). Therefore, we only check type 
compatibility.
+                      */
 -                    final var result = ((Operation) property).getResult();
 -                    if (result instanceof AttributeType<?>) {
 -                        final Class<?> c = ((AttributeType<?>) 
result).getValueClass();
++                    final var result = ((AbstractOperation) 
property).getResult();
++                    if (result instanceof DefaultAttributeType<?>) {
++                        final Class<?> c = ((DefaultAttributeType<?>) 
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.
+                             if (Features.getLinkTarget(property).isPresent() 
? r.isAssignableFrom(c) : r.equals(c)) {
+                                 return true;
+                             }
+                             throw new 
UnconvertibleObjectException(Errors.forLocale(getLocale())
+                                         
.getString(Errors.Keys.CanNotConvertFromType_2, c, r));
+                         }
+                     }
+                 }
+             }
+             return false;
+         }
+ 
+         /**
+          * Sets the expression to use for evaluating the property value.
+          * If {@code stored} is {@code true} (the usual case), then the 
expression will be evaluated early
+          * and its result will be stored as an attribute value, unless this 
property is not an attribute.
+          * If {@code stored} is {@code false}, this method replaces the 
attribute by an operation wrapping
+          * the given expression. In other words, the evaluation of the 
expression will be deferred.
+          * The latter case is possible only if the {@code FeatureType} 
contains all dependencies
+          * that the operation needs.
+          *
+          * @param  expression  the expression to be evaluated by the 
operation.
+          */
 -        public void setValueGetter(final Expression<? super Feature, ?> 
expression, final boolean stored) {
++        public void setValueGetter(final Expression<? super AbstractFeature, 
?> expression, final boolean stored) {
+             if (builder instanceof AttributeTypeBuilder<?>) {
+                 if (stored) {
+                     attributeValueGetter = expression;
+                 } else {
+                     final var atb = (AttributeTypeBuilder<?>) builder;
+                     /*
+                      * Optimization: we could compute `storedType = 
atb.build()` unconditionally,
+                      * which creates an attribute with the final name in the 
target feature type.
+                      * However, in the particular case of links, we are 
better to use the name of
+                      * the property in the source feature type, because it 
allows an optimization
+                      * in `ExpressionOperation.create(…)` (a replacement by a 
`LinkOperation`).
+                      */
 -                    AttributeType<?> storedType = null;
++                    DefaultAttributeType<?> storedType = null;
+                     if (expression instanceof ValueReference<?,?>) {
+                         var candidate = 
source.getProperty(((ValueReference<?,?>) expression).getXPath());
 -                        if (candidate instanceof AttributeType<?>) {
 -                            storedType = (AttributeType<?>) candidate;
++                        if (candidate instanceof DefaultAttributeType<?>) {
++                            storedType = (DefaultAttributeType<?>) candidate;
+                         }
+                     }
+                     if (storedType == null) {
+                         storedType = atb.build();   // Same name as in the 
`identification` map below.
+                     }
+                     final var identification = 
Map.of(AbstractOperation.NAME_KEY, builder.getName());
+                     builder = 
addProperty(FeatureOperations.expression(identification, expression, 
storedType));
+                     atb.replaceBy(builder);
+                     hasModifiedProperties = true;
+                 }
+             } else {
+                 // The property is an operation, usually a link. Leave it 
as-is.
+             }
+         }
+ 
+         /**
+          * Returns the expression for evaluating the value to store in the 
attribute built by this item.
+          * The expression may be {@code null} if the value is computed 
on-the-fly (i.e. the property is
+          * an operation), or if the expression has not been specified.
+          */
 -        final Expression<? super Feature, ?> attributeValueGetter() {
++        final Expression<? super AbstractFeature, ?> attributeValueGetter() {
+             return attributeValueGetter;
+         }
+ 
+         /**
+          * Sets the coordinate reference system that characterizes the values 
of this attribute.
+          *
+          * @param  crs  coordinate reference system associated to attribute 
values, or {@code null}.
+          * @return {@code this} for method calls chaining.
+          */
+         public Item setCRS(final CoordinateReferenceSystem crs) {
+             if (builder instanceof AttributeTypeBuilder<?>) {
+                 builder = ((AttributeTypeBuilder<?>) builder).setCRS(crs);
+                 hasModifiedProperties = true;
+             }
+             return this;
+         }
+ 
+         /**
+          * Returns whether the property built by this item is equivalent to 
the given property.
+          * The caller should have verified that {@link 
#hasModifiedProperties} is {@code false}
+          * before to invoke this method, because the implementation performs 
a filtering based
+          * on the property name only. This is that way for accepting 
differences in metadata.
+          *
+          * @param  property  the property to compare.
+          * @return whether this item builds a property equivalent to the 
given one.
+          *
+          * @see #isIdentity()
+          */
 -        private boolean equivalent(final PropertyType property) {
++        private boolean equivalent(final AbstractIdentifiedType property) {
+             return builder.getName().equals(property.getName());
+         }
+ 
+         /**
+          * Returns the name of the projected property.
+          * This is initially the name of the property given at construction 
time,
+          * but can be changed later by a call to {@link 
#setName(GenericName)}.
+          *
+          * @return the name of the projected property.
+          */
+         public String getName() {
+             return builder.getName().toString();
+         }
+ 
+         /**
+          * Sets the name of the projected property. A {@code null} argument 
means that the name is unspecified,
+          * in which case a different name may be generated later if the 
current name collides with other names.
+          *
+          * <p>This method should be invoked exactly once for each item, even 
if the argument is {@code null}.
+          * The reason is because this method uses this information for 
recording which names to reserve.</p>
+          *
+          * @param  targetName  the desired name in the projected feature, or 
{@code null} if unspecified.
+          */
+         public void setName(final GenericName targetName) {
+             if (targetName == null) {
+                 reserve(sourceName, null);      // Will use that name only if 
not owned by another item.
+                 preferCurrentName = true;
+             } else if (targetName.equals(sourceName)) {
+                 reserve(sourceName, this);      // Take possession of that 
name.
+                 isNamed = true;
+             } else {
+                 builder.setName(targetName);
+                 reserve(targetName, this);
+                 hasModifiedProperties = true;   // Because the name is 
different.
+                 isNamed = true;
+             }
+         }
+ 
+         /**
+          * If this item has not received an explicit name, infers a default 
name.
+          * This method should be invoked only after {@link 
#setName(GenericName)}
+          * has been invoked for all items, for allowing this class to know 
which
+          * names are reserved.
+          */
+         private void validateName() {
+             if (!isNamed) {
+                 final Item owner = reservedNames.get(sourceName);
+                 if (owner != this) {
+                     GenericName name = sourceName;
+                     if (owner != null || name == null || (!preferCurrentName 
&& reservedNames.containsKey(name))) {
+                         do {
+                             var text = 
Vocabulary.formatInternational(Vocabulary.Keys.Unnamed_1, ++unnamedNumber);
+                             name = builder.setName(text).getName();     // 
Local name with the appropriate name space.
+                         } while (reservedNames.containsKey(name));      // 
Reminder: the associated value may be null.
+                     }
+                     reserve(name, this);
+                 }
+                 isNamed = true;
+             }
+         }
+     }
+ 
+     /**
+      * Declares the given name as reserved. If this class needs to generate a 
default name,
+      * it will ensure that automatically generated names do not conflict with 
reserved names.
+      *
+      * @param  name   name to reserve for a projected property type, or 
{@code null} if none.
+      * @param  owner  the builder using that name, or {@code null} if none.
+      */
+     private void reserve(GenericName name, final Item owner) {
+         if (name != null) {
+             // By `putIfAbsent` method contract, non-null values have 
precedence over null values.
+             reservedNames.putIfAbsent(name, owner);
+             if (name != (name = name.tip())) {              // Shortcut for a 
majority of cases.
+                 reservedNames.putIfAbsent(name, owner);
+             }
+         }
+     }
+ 
+     /**
+      * Adds dependencies. This method adds in the {@code deferred} list any 
transitive
+      * dependencies which may need to be added in a second pass after this 
method call.
+      * The elements added into {@code deferred} are {@linkplain #source} 
properties.
+      *
+      * @param  deferred  where to add missing transitive dependencies (source 
properties).
+      */
 -    private void resolveDependencies(final List<PropertyType> deferred) {
++    private void resolveDependencies(final List<AbstractIdentifiedType> 
deferred) {
+         final var it = dependencies.entrySet().iterator();
+         while (it.hasNext()) {
+             final Map.Entry<String, List<Item>> entry = it.next();
 -            final PropertyType property = source.getProperty(entry.getKey());
++            final AbstractIdentifiedType property = 
source.getProperty(entry.getKey());
+             final GenericName sourceName = property.getName();
+             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.");
+                 }
+             } else {
+                 for (Item dependent : entry.getValue()) {
+                     dependent.hasMissingDependency = true;
+                 }
+                 deferred.add(property);
+             }
+             it.remove();
+         }
+     }
+ 
+     /**
+      * Returns {@code true} if the feature to be built should be equivalent 
to the source feature.
+      *
+      * @return whether the {@linkplain #source} feature type can be used 
directly.
+      */
+     private boolean isIdentity() {
+         if (hasModifiedProperties) {
+             return false;
+         }
+         final Iterator<Item> it = requested.iterator();
 -        for (PropertyType property : source.getProperties(true)) {
++        for (AbstractIdentifiedType property : source.getProperties(true)) {
+             if (!(it.hasNext() && it.next().equivalent(property))) {
+                 return false;
+             }
+         }
+         return !it.hasNext();
+     }
+ 
+     /**
+      * Returns the feature type described by this builder. This method may 
return the
+      * {@linkplain #source() source} directly if this projection performs no 
operation.
+      */
+     @Override
 -    public FeatureType build() {
++    public DefaultFeatureType build() {
+         return isIdentity() ? source : super.build();
+     }
+ 
+     /**
+      * Sets the default name of all anonymous properties, then builds the 
feature types.
+      * Two feature types are built: one with only the requested properties, 
and another
+      * type augmented with dependencies of operations such as links.
+      *
+      * <p>This method should be invoked exactly once.</p>
+      *
+      * <h4>Identity operation</h4>
+      * If the result is a feature type with all the properties of the source 
feature,
+      * with the same property names in the same order, and if the expressions 
are only
+      * fetching the values (no computation), then this method returns an 
empty value
+      * for meaning that this projection does nothing.
+      *
+      * @return the feature types with and without dependencies, or empty if 
there is no projection.
+      */
+     public Optional<FeatureProjection> project() {
+         requested.forEach(Item::validateName);
+         /*
+          * Add properties for all dependencies that are required by link 
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();
+         final int count = properties.size();
 -        final var deferred = new ArrayList<PropertyType>();
++        final var deferred = new ArrayList<AbstractIdentifiedType>();
+         resolveDependencies(deferred);
+         /*
+          * If there is no dependencies, the requested type and the type with 
dependencies are the same.
+          * Otherwise, we need to resolve transitive dependencies before to 
build each type.
+          */
 -        final FeatureType typeRequested, typeWithDependencies;
++        final DefaultFeatureType typeRequested, typeWithDependencies;
+         if (deferred.isEmpty()) {
+             typeRequested = typeWithDependencies = build();
+         } else {
+             do {
 -                for (PropertyType property : deferred) {
++                for (AbstractIdentifiedType property : deferred) {
+                     final Item item = addSourceProperty(property, true);
+                     if (item != null) {
+                         item.validateName();
+                         
item.setValueGetter(FeatureOperations.expressionOf(property), true);
+                     }
+                 }
+                 deferred.clear();
+                 resolveDependencies(deferred);
+             } while (!deferred.isEmpty());
+             typeWithDependencies = build();
+             properties.subList(count, properties.size()).clear();     // Keep 
only the properties requested by user.
+             requested.forEach(Item::replaceIfMissingDependency);
+             typeRequested = build();
+         }
+         if (source.equals(typeRequested) && 
source.equals(typeWithDependencies)) {
+             return Optional.empty();
+         }
+         return Optional.of(new FeatureProjection(typeRequested, 
typeWithDependencies, requested));
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureView.java
index 0000000000,a0dac0a57f..f8c5a97a49
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureView.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureView.java
@@@ -1,0 -1,131 +1,129 @@@
+ /*
+  * Licensed to the Apache Software Foundation (ASF) under one or more
+  * contributor license agreements.  See the NOTICE file distributed with
+  * this work for additional information regarding copyright ownership.
+  * The ASF licenses this file to You under the Apache License, Version 2.0
+  * (the "License"); you may not use this file except in compliance with
+  * the License.  You may obtain a copy of the License at
+  *
+  *     http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS,
+  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  * See the License for the specific language governing permissions and
+  * limitations under the License.
+  */
+ package org.apache.sis.feature.privy;
+ 
+ import org.apache.sis.feature.AbstractFeature;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.Property;
++// Specific to the main branch:
++import org.apache.sis.feature.DefaultFeatureType;
+ 
+ 
+ /**
+  * A feature containing a subset of the properties of another feature.
+  * The feature type of the view must be specified in argument together with 
the source feature instance.
+  * All properties that are present in this feature view shall have the same 
name as in the source feature.
+  * This class does not verify that requirement.
+  *
+  * <h2>Limitations</h2>
+  * For performance and simplicity reasons, the current implementation does 
not prevent users
+  * from requesting a property that exists in the full feature instance but 
not in this subset.
+  *
+  * <h2>Possible evolution</h2>
+  * It would be possible for the view to contain operations that are not 
present in the source features.
+  * This extension has not yet been implemented because not yet needed.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  */
+ final class FeatureView extends AbstractFeature {
+     /**
+      * For cross-version compatibility.
+      */
+     private static final long serialVersionUID = -4299168913599597991L;
+ 
+     /**
+      * The instance with all properties.
+      */
+     @SuppressWarnings("serial")     // Apache SIS implementations are 
serializable.
 -    private final Feature source;
++    private final AbstractFeature source;
+ 
+     /**
+      * Creates a new feature instance which is a subset of the given {@code 
source} feature.
+      * This constructor does not verify the consistency of the two arguments.
+      *
+      * @param  subset  the feature type that describes the feature subset.
+      * @param  source  the complete feature instance to view as a subset.
+      */
 -    FeatureView(final FeatureType subset, final Feature source) {
++    FeatureView(final DefaultFeatureType subset, final AbstractFeature 
source) {
+         super(subset);
+         this.source = source;
+     }
+ 
+     /**
+      * Returns the property (attribute, feature association or operation 
result) of the given name.
+      * This method delegates to the wrapped source without checking whether 
the given name exists in this subset.
+      *
+      * @param  name  the property name.
+      * @return the property of the given name (never {@code null}).
+      */
+     @Override
 -    public Property getProperty(final String name) {
++    public Object getProperty(final String name) {
+         return source.getProperty(name);
+     }
+ 
+     /**
+      * Sets the property (attribute or feature association).
+      * This method delegates to the wrapped source without checking whether 
the given name exists in this subset.
+      *
+      * @param  property  the property to set.
+      */
+     @Override
 -    public void setProperty(final Property property) {
++    public void setProperty(final Object property) {
+         source.setProperty(property);
+     }
+ 
+     /**
+      * Returns the value for the property of the given name.
+      * This method delegates to the wrapped source without checking whether 
the given name exists in this subset.
+      *
+      * @param  name  the property name.
+      * @return value of the specified property, or the default value (which 
may be {@code null}} if none.
+      */
+     @Override
+     public Object getPropertyValue(final String name) {
+         return source.getPropertyValue(name);
+     }
+ 
+     /**
+      * Sets the value for the property of the given name.
+      * This method delegates to the wrapped source without checking whether 
the given name exists in this subset.
+      *
+      * @param  name   the property name.
+      * @param  value  the new value for the specified property (may be {@code 
null}).
+      */
+     @Override
+     public void setPropertyValue(final String name, final Object value) {
+         source.setPropertyValue(name, value);
+     }
+ 
+     /**
+      * Returns the value for the property of the given name if that property 
exists, or a fallback value otherwise.
+      * This method delegates to the wrapped source without checking whether 
the given name exists in this subset.
+      *
+      * <h4>Design note</h4>
+      * We could add a verification of whether the property exists in the 
feature type given by {@link #getType()}.
+      * We don't do that for now because the current usages of this method in 
the Apache SIS code base do not need
+      * this method to be strict, and for consistency with the behavior of 
other methods in this class.
+      *
+      * @param  name  the property name.
+      * @param  missingPropertyFallback  the value to return if no attribute 
or association of the given name exists.
+      * @return value or default value of the specified property, or {@code 
missingPropertyFallback}.
+      */
+     @Override
+     public Object getValueOrFallback(final String name, final Object 
missingPropertyFallback) {
+         return source.getValueOrFallback(name, missingPropertyFallback);
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
index cddc6af90c,eabd8cdf58..1ad4f4f944
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
@@@ -27,9 -26,9 +26,8 @@@ import org.apache.sis.util.Unconvertibl
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.math.Fraction;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.AttributeType;
 -import org.opengis.filter.Expression;
 +// Specific to the main branch:
- import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.feature.DefaultAttributeType;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
index 8e44067fff,1ab716a0a3..43b9a49c15
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
@@@ -22,18 -22,17 +22,17 @@@ import java.util.List
  import java.util.Collection;
  import java.util.Optional;
  import org.apache.sis.feature.Features;
- import org.apache.sis.feature.builder.FeatureTypeBuilder;
- import org.apache.sis.feature.builder.PropertyTypeBuilder;
+ import org.apache.sis.feature.privy.FeatureProjectionBuilder;
  import org.apache.sis.math.FunctionProperty;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.PropertyNotFoundException;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
 +// Specific to the main branch:
 +import org.opengis.util.ScopedName;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.AbstractIdentifiedType;
 +import org.apache.sis.feature.DefaultAssociationRole;
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.pending.geoapi.filter.Name;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
  
  
  /**
@@@ -207,32 -201,33 +206,33 @@@ walk:   if (specifiedType != null) try 
      }
  
      /**
-      * Provides the expected type of values produced by this expression when 
a feature of the given type is evaluated.
+      * Provides the expected type of values produced by this expression. This 
method delegates to
+      * {@link #accessor} after setting the source feature type to the tip of 
{@code "a/b/c"} path.
       *
-      * @param  valueType  the type of features to be evaluated by the given 
expression.
-      * @param  addTo      where to add the type of properties evaluated by 
the given expression.
+      * @param  addTo  where to add the type of properties evaluated by this 
expression.
       * @return builder of the added property, or {@code null} if this method 
cannot add a property.
-      * @throws IllegalArgumentException if this method cannot determine the 
property type for the given feature type.
 -     * @throws PropertyNotFoundException if the property was not found in 
{@code addTo.source()}.
++     * @throws IllegalArgumentException if the property was not found in 
{@code addTo.source()}.
       */
      @Override
-     public PropertyTypeBuilder expectedType(DefaultFeatureType valueType, 
final FeatureTypeBuilder addTo) {
+     public FeatureProjectionBuilder.Item expectedType(final 
FeatureProjectionBuilder addTo) {
 -        FeatureType valueType = addTo.source();
++        DefaultFeatureType valueType = addTo.source();
          for (final String p : path) {
 -            final PropertyType type;
 +            final AbstractIdentifiedType type;
              try {
                  type = valueType.getProperty(p);
 -            } catch (PropertyNotFoundException e) {
 +            } catch (IllegalArgumentException e) {
                  if (accessor.isVirtual) {
                      // The association does not exist but may be defined on a 
yet unknown child type.
-                     return accessor.expectedType(addTo);
+                     return accessor.defaultType(addTo);
                  }
                  throw e;
              }
 -            if (!(type instanceof FeatureAssociationRole)) {
 +            if (!(type instanceof DefaultAssociationRole)) {
                  return null;
              }
 -            valueType = ((FeatureAssociationRole) type).getValueType();
 +            valueType = ((DefaultAssociationRole) type).getValueType();
          }
-         return accessor.expectedType(valueType, addTo);
+         return addTo.using(valueType, accessor);
      }
  
      /**
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
index 3275c03e5d,c614215ed9..354bac4286
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
@@@ -30,9 -28,9 +28,6 @@@ import org.apache.sis.feature.privy.Fea
  import org.apache.sis.math.FunctionProperty;
  import org.apache.sis.util.resources.Errors;
  
- // Specific to the main branch:
- import org.apache.sis.feature.DefaultFeatureType;
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Expression;
--
  
  /**
   * Expression whose results are converted to a different type.
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
index 03264883a2,9133bec01e..1c39a4bd3b
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
@@@ -32,13 -33,16 +33,12 @@@ import org.apache.sis.filter.sqlmm.Regi
  import org.apache.sis.util.ArgumentChecks;
  import org.apache.sis.util.iso.AbstractFactory;
  import org.apache.sis.util.resources.Errors;
--import org.apache.sis.util.privy.Strings;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import java.util.Iterator;
 -import java.time.Instant;
 -import org.opengis.filter.*;
 -import org.opengis.feature.Feature;
 -import org.opengis.filter.capability.AvailableFunction;
 -import org.opengis.filter.capability.FilterCapabilities;
 -import org.apache.sis.util.privy.AbstractMap;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.pending.geoapi.filter.MatchAction;
 +import org.apache.sis.pending.geoapi.filter.SpatialOperatorName;
 +import org.apache.sis.pending.geoapi.filter.DistanceOperatorName;
  
  
  /**
@@@ -142,8 -139,44 +142,32 @@@ public abstract class DefaultFilterFact
          return Features.DEFAULT;
      }
  
+     /**
+      * Returns a factory operating on resource instances of the given class.
+      * The current implementation recognizes the following classes:
+      *
+      * <ul>
+      *   <li>{@link Feature}: delegate to {@link #forFeatures()}.</li>
+      * </ul>
+      *
+      * More classes may be added in future versions.
+      *
+      * @param  <R>   compile-time value of the {@code type} argument.
+      * @param  type  type of resources that the factory shall accept.
+      * @return factory operating on resource instances of the given class.
+      * @since 1.5
+      */
+     @SuppressWarnings("unchecked")
 -    public static <R> Optional<FilterFactory<R, Object, Object>> 
forResources(final Class<R> type) {
 -        if (type.equals(Feature.class)) {
 -            return Optional.of((FilterFactory<R, Object, Object>) 
forFeatures());
++    public static <R> Optional<DefaultFilterFactory<R, Object, Object>> 
forResources(final Class<R> type) {
++        if (type.equals(AbstractFeature.class)) {
++            return Optional.of((DefaultFilterFactory<R, Object, Object>) 
forFeatures());
+         } else {
+             return Optional.empty();
+         }
+     }
+ 
      /**
 -     * Describes the abilities of this factory. The description includes 
restrictions on
 -     * the available spatial operations, scalar operations, lists of 
supported functions,
 -     * and description of which geometry literals are understood.
 -     *
 -     * @return description of the abilities of this factory.
 -     */
 -    @Override
 -    public FilterCapabilities getCapabilities() {
 -        return new Capabilities(this);              // Cheap to construct, no 
need to cache.
 -    }
 -
 -    /**
 -     * A filter factory operating on {@link Feature} instances.
 +     * A filter factory operating on {@link AbstractFeature} instances.
       *
       * @param  <G>  base class of geometry objects. The 
implementation-neutral type is GeoAPI {@link Geometry},
       *              but this factory allows the use of other implementations 
such as JTS
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
index 0000000000,3a49222670..86379fdf59
mode 000000,100644..100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
@@@ -1,0 -1,81 +1,78 @@@
+ /*
+  * Licensed to the Apache Software Foundation (ASF) under one or more
+  * contributor license agreements.  See the NOTICE file distributed with
+  * this work for additional information regarding copyright ownership.
+  * The ASF licenses this file to You under the Apache License, Version 2.0
+  * (the "License"); you may not use this file except in compliance with
+  * the License.  You may obtain a copy of the License at
+  *
+  *     http://www.apache.org/licenses/LICENSE-2.0
+  *
+  * Unless required by applicable law or agreed to in writing, software
+  * distributed under the License is distributed on an "AS IS" BASIS,
+  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  * See the License for the specific language governing permissions and
+  * limitations under the License.
+  */
+ package org.apache.sis.filter;
+ 
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.InvalidFilterValueException;
 -
+ 
+ /**
+  * Exceptions thrown when the XPath in an expression is invalid or 
unsupported.
+  * Apache SIS currently supports only a small subset of XPath syntax, mostly 
paths of
+  * the form {@code "a/b/c"} (and not everywhere) and the {@code 
"Q{namespace}"} syntax.
+  *
+  * <h4>Relationship with standard libraries</h4>
+  * The standard Java libraries provides a {@link 
javax.xml.xpath.XPathException}.
+  * This {@code InvalidXPathException} differs in that it is an unchecked 
exception
+  * and is thrown in the context of operations with OGC filters and 
expressions.
+  * In some implementations, {@code InvalidXPathException} may have a {@code 
XPathException} as its cause.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.5
+  *
+  * @see javax.xml.xpath.XPathException
+  *
+  * @since 1.5
+  */
 -public class InvalidXPathException extends InvalidFilterValueException {
++public class InvalidXPathException extends IllegalArgumentException {
+     /**
+      * Serial number for inter-operability with different versions.
+      */
+     private static final long serialVersionUID = 1654277877397802378L;
+ 
+     /**
+      * Creates an exception with no message.
+      */
+     public InvalidXPathException() {
+         super();
+     }
+ 
+     /**
+      * Creates an exception with the specified message.
+      *
+      * @param message  the detail message, saved for later retrieval by the 
{@link #getMessage()} method.
+      */
+     public InvalidXPathException(final String message) {
+         super(message);
+     }
+ 
+     /**
+      * Creates an exception with the specified cause.
+      *
+      * @param cause  the cause, saved for later retrieval by the {@link 
#getCause()} method.
+      */
+     public InvalidXPathException(final Throwable cause) {
+         super(cause);
+     }
+ 
+     /**
+      * Creates an exception with the specified message and cause.
+      *
+      * @param message  the detail message, saved for later retrieval by the 
{@link #getMessage()} method.
+      * @param cause    the cause, saved for later retrieval by the {@link 
#getCause()} method.
+      */
+     public InvalidXPathException(final String message, final Throwable cause) 
{
+         super(message, cause);
+     }
+ }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
index a502cbb779,a869d79ae4..0c9958f264
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
@@@ -27,14 -27,13 +27,12 @@@ import org.apache.sis.util.iso.Names
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.util.collection.WeakValueHashMap;
  import org.apache.sis.feature.privy.FeatureExpression;
+ import org.apache.sis.feature.privy.FeatureProjectionBuilder;
  import org.apache.sis.filter.internal.Node;
- import org.apache.sis.feature.builder.FeatureTypeBuilder;
- import org.apache.sis.feature.builder.PropertyTypeBuilder;
  import org.apache.sis.math.FunctionProperty;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.AttributeType;
 -import org.opengis.filter.Expression;
 +// Specific to the main branch:
- import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.feature.DefaultAttributeType;
  
  
  /**
@@@ -154,16 -149,17 +152,17 @@@ abstract class LeafExpression<R,V> exte
          }
  
          /**
-          * Provides the type of values returned by {@link #apply(Object)}
-          * wrapped in an {@link DefaultAttributeType} named "Literal".
+          * Provides the type of values returned by {@link #apply(Object)}.
 -         * The returned item wraps an {@link AttributeType} named "Literal".
++         * The returned item wraps an {@code AttributeType} named "Literal".
+          * The attribute type is determined by the class of the {@linkplain 
#value}.
           *
           * @param  addTo  where to add the type of properties evaluated by 
the given expression.
-          * @return builder of the added property.
+          * @return handler for the added property.
           */
          @Override
-         public PropertyTypeBuilder expectedType(DefaultFeatureType ignored, 
final FeatureTypeBuilder addTo) {
+         public FeatureProjectionBuilder.Item expectedType(final 
FeatureProjectionBuilder addTo) {
              final Class<?> valueType = getValueClass();
 -            AttributeType<?> propertyType;
 +            DefaultAttributeType<?> propertyType;
              synchronized (TYPES) {
                  propertyType = TYPES.get(valueType);
                  if (propertyType == null) {
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
index 9fc8e48551,06410e94c4..489c777fbd
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
@@@ -29,15 -27,13 +27,14 @@@ import org.apache.sis.feature.privy.Fea
  import org.apache.sis.filter.privy.XPath;
  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.FeatureType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.PropertyNotFoundException;
 -import org.opengis.filter.ValueReference;
 +// Specific to the main branch:
 +import org.opengis.util.ScopedName;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.AbstractIdentifiedType;
- import org.apache.sis.feature.AbstractOperation;
 +import org.apache.sis.feature.DefaultAttributeType;
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.pending.geoapi.filter.Name;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
  
  
  /**
@@@ -375,21 -366,27 +375,27 @@@ abstract class PropertyValue<V> extend
  
      /**
       * Provides the expected type of values produced by this expression when 
a feature of the given type is evaluated.
+      * The source feature type is specified indirectly by {@link 
FeatureProjectionBuilder#source()}.
       *
-      * @param  valueType  the type of features to be evaluated by the given 
expression.
-      * @param  addTo      where to add the type of properties evaluated by 
the given expression.
+      * <h4>Handling of operations</h4>
+      * Properties that are operations are replaced by attributes where the 
operation result will be stored.
+      * An exception to this rule is the links such as {@code 
"sis:identifier"} and {@code "sis:geometry"},
+      * in which case the link operation is kept. It may force {@code 
FeatureProjectionBuilder} to add also
+      * the dependencies (targets) of the link.
+      *
+      * @param  addTo  where to add the type of properties evaluated by this 
expression.
       * @return builder of the added property, or {@code null} if this method 
cannot add a property.
 -     * @throws PropertyNotFoundException if the property was not found in 
{@code addTo.source()}.
 +     * @throws IllegalArgumentException if this method cannot determine the 
property type for the given feature type.
       */
      @Override
-     public PropertyTypeBuilder expectedType(final DefaultFeatureType 
valueType, final FeatureTypeBuilder addTo) {
+     public FeatureProjectionBuilder.Item expectedType(final 
FeatureProjectionBuilder addTo) {
 -        PropertyType type;
 +        AbstractIdentifiedType type;
          try {
-             type = valueType.getProperty(name);
+             type = addTo.source().getProperty(name);
 -        } catch (PropertyNotFoundException e) {
 +        } catch (IllegalArgumentException e) {
              if (isVirtual) {
-                 // The property does not exist but may be defined on a yet 
unknown child type.
-                 return expectedType(addTo);
+                 // The property does not exist but may be defined in a yet 
unknown child type.
+                 return defaultType(addTo);
              }
              throw e;
          }
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
index 40728be85a,2d751dd31e..82b5e59df7
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
@@@ -30,10 -28,10 +28,9 @@@ import org.apache.sis.feature.privy.Fea
  import org.apache.sis.util.privy.Constants;
  import org.apache.sis.util.resources.Errors;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.InvalidFilterValueException;
 +// Specific to the main branch:
- import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.pending.geoapi.filter.Literal;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
index f4f27cc967,b938087a8a..714288c7f8
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
@@@ -32,9 -30,9 +30,8 @@@ import org.apache.sis.util.ArgumentChec
  import org.apache.sis.util.resources.Errors;
  import org.apache.sis.util.iso.Names;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.InvalidFilterValueException;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Expression;
- import org.apache.sis.feature.DefaultFeatureType;
  
  
  /**
@@@ -180,33 -178,28 +177,28 @@@ abstract class SpatialFunction<R> exten
       *   <li>Otherwise an attribute is created with the return value 
specified by the operation.</li>
       * </ul>
       *
-      * @param  valueType  the type of features on which to apply this 
expression.
-      * @param  addTo      where to add the type of properties evaluated by 
this expression.
+      * @param  addTo  where to add the type of properties evaluated by this 
expression.
       * @return builder of type resulting from expression evaluation (never 
null).
-      * @throws IllegalArgumentException if the given feature type does not 
contain the expected properties,
 -     * @throws InvalidFilterValueException if the source feature type does 
not contain the expected properties,
++     * @throws IllegalArgumentException if the source feature type does not 
contain the expected properties,
       *         or if this method cannot determine the result type of the 
expression.
       *         It may be because that expression is backed by an unsupported 
implementation.
       */
      @Override
-     public PropertyTypeBuilder expectedType(final DefaultFeatureType 
valueType, final FeatureTypeBuilder addTo) {
-         AttributeTypeBuilder<?> att;
- cases:  if (operation.isGeometryInOut()) {
+     public FeatureProjectionBuilder.Item expectedType(final 
FeatureProjectionBuilder addTo) {
+         if (operation.isGeometryInOut()) {
              final FeatureExpression<?,?> fex = 
FeatureExpression.castOrCopy(getParameters().get(0));
              if (fex != null) {
-                 final PropertyTypeBuilder type = fex.expectedType(valueType, 
addTo);
-                 if (type instanceof AttributeTypeBuilder<?>) {
-                     att = (AttributeTypeBuilder<?>) type;
-                     final Geometries<?> library = 
Geometries.factory(att.getValueClass());
-                     if (library != null) {
-                         att = 
att.setValueClass(operation.getReturnType(library));
-                         break cases;
-                     }
+                 final FeatureProjectionBuilder.Item item = 
fex.expectedType(addTo);
+                 final boolean success = item.replaceValueClass((c) -> {
+                     final Geometries<?> library = Geometries.factory(c);
+                     return (library == null) ? null : 
operation.getReturnType(library);
+                 });
+                 if (success) {
+                     return item;
                  }
              }
 -            throw new 
InvalidFilterValueException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression));
 +            throw new 
IllegalArgumentException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression));
-         } else {
-             att = addTo.addAttribute(getValueClass());
          }
-         return att.setName(getFunctionName());
+         return 
addTo.addComputedProperty(addTo.addAttribute(getValueClass()).setName(getFunctionName()),
 false);
      }
  }
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
index 7ff19f8852,4636952051..83dc035fcc
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
@@@ -25,14 -25,14 +25,14 @@@ import java.sql.PreparedStatement
  import java.sql.ResultSet;
  import java.sql.SQLException;
  import org.apache.sis.metadata.sql.privy.SQLBuilder;
- import org.apache.sis.storage.base.FeatureProjection;
+ import org.apache.sis.feature.privy.FeatureProjection;
  import org.apache.sis.util.collection.WeakValueHashMap;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.filter.SortOrder;
 -import org.opengis.filter.SortProperty;
 -import org.opengis.filter.SortBy;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.pending.geoapi.filter.SortOrder;
 +import org.apache.sis.pending.geoapi.filter.SortProperty;
 +import org.apache.sis.pending.geoapi.filter.SortBy;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
index 4e81c0694d,2386a8fa35..963613ab67
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
@@@ -41,13 -39,12 +39,12 @@@ import org.apache.sis.util.privy.String
  import org.apache.sis.util.stream.DeferredStream;
  import org.apache.sis.util.stream.PaginedStream;
  import org.apache.sis.storage.DataStoreException;
- import org.apache.sis.storage.base.FeatureProjection;
+ import org.apache.sis.feature.privy.FeatureProjection;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.SortBy;
 +// Specific to the main branch:
 +import org.apache.sis.filter.Filter;
- import org.apache.sis.filter.Expression;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.pending.geoapi.filter.SortBy;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
index 1af3574bf7,9b7981d788..7110ac76a7
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
@@@ -57,9 -57,10 +57,10 @@@ import org.apache.sis.util.privy.Consta
  import org.apache.sis.io.wkt.Convention;
  import org.apache.sis.io.wkt.WKTFormat;
  import org.apache.sis.io.wkt.Warnings;
+ import org.apache.sis.util.Workaround;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.metadata.Identifier;
 +// Specific to the main branch:
 +import org.opengis.referencing.ReferenceIdentifier;
  
  
  /**
diff --cc 
endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
index 7d53b4d7d6,1a56e7149e..118a8f666c
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
@@@ -43,11 -46,12 +46,10 @@@ import org.apache.sis.util.collection.T
  import org.apache.sis.util.iso.DefaultNameSpace;
  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.FeatureType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.FeatureAssociationRole;
 -import org.opengis.filter.InvalidFilterValueException;
 +// Specific to the main branch:
- import org.apache.sis.filter.Expression;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.feature.DefaultAssociationRole;
  
  
  /**
@@@ -76,9 -80,16 +78,16 @@@ final class Table extends AbstractFeatu
       * except synthetic attributes like "sis:identifier". The feature may 
also contain associations
       * inferred from foreigner keys that are not immediately apparent in the 
table.
       *
+      * <h4>Relationship with {@code FeatureSet.getType()}</h4>
+      * When this {@code Table} has been created by inspection of the database 
metadata, this feature type
+      * is the value shown to users. But when this {@code Table} has been 
created from a query, this value
+      * is not directly shown to user because it may contain additional 
properties for the dependencies of
+      * operations. In the latter case, this table is hidden behind {@link 
FeatureIterator} and the type
+      * seen by user is provided by {@link 
org.apache.sis.storage.FeatureSubset#resultType}.
+      *
       * @see #getType()
       */
 -    final FeatureType featureType;
 +    final DefaultFeatureType featureType;
  
      /**
       * The SQL query to execute for fetching data, or {@code null} for 
querying the table identified by {@link #name}.
@@@ -193,13 -204,13 +202,13 @@@
       * Creates a new table as a projection (subset of columns) of the given 
table.
       * The columns to retain, potentially under different names, are 
specified in {@code projection}.
       * The projection may also contain complex expressions that cannot be 
handled by this constructor.
-      * Such expressions are stored in the {@code unhandled} map.
+      * Such expressions have their indexes stored in the {@code unhandled} 
set.
       *
-      * @param  table        the source table.
+      * @param  source       the source table.
       * @param  projection   description of the columns to keep.
       * @param  reusedNames  an initially empty set where to store the names 
of attributes that are not renamed.
-      * @param  unhandled    an initially empty map where to add expressions 
that are not handled by the new table.
+      * @param  unhandled    where to set the bits for indexes of expressions 
that are not handled by the new table.
 -     * @throws InvalidFilterValueException if there is an error in the 
declaration of property values.
 +     * @throws IllegalArgumentException if there is an error in the 
declaration of property values.
       */
      @SuppressWarnings("LocalVariableHidesMemberVariable")
      Table(final Table source,
@@@ -384,9 -395,11 +393,11 @@@
  
      /**
       * Returns the feature type inferred from the database structure analysis.
+      * Note that this type may not be the type shown to user if this table is
+      * created behind a {@link org.apache.sis.storage.FeatureSubset}.
       */
      @Override
 -    public final FeatureType getType() {
 +    public final DefaultFeatureType getType() {
          return featureType;
      }
  
diff --cc 
endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
index fe55cf6f5c,62b25931c2..6b88e592f4
--- 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
@@@ -439,6 -445,30 +441,30 @@@ public final class SQLStoreTest extend
          assertEquals("FeatureStream[table=“Countries”, predicates=“SQL”]", 
executionMode);
      }
  
+     /**
+      * Requests a new set of features with a subset containing the 
"sis:identifier" property.
+      * The difficulty is that no column named "sis:column" exists. The 
property is a link to
+      * the "code" attribute, which is intentionally omitted in the projection 
(in SQL sense)
+      * in order to test the capability to follow dependencies.
+      *
+      * @param  dataset  the store on which to query the features.
+      * @throws DataStoreException if an error occurred during query execution.
+      */
+     private void verifyLinkInProjection(final SimpleFeatureStore dataset) 
throws Exception {
+         final var query = new FeatureQuery();
+         query.setProjection("sis:identifier", "native_name");
+         final FeatureSet countries = 
dataset.findResource("Countries").subset(query);
+         List<Object> names;
 -        try (Stream<Feature> features = countries.features(false)) {
++        try (Stream<AbstractFeature> features = countries.features(false)) {
+             names = features.map(f -> 
f.getPropertyValue("sis:identifier")).toList();
+         }
+         assertSetEquals(List.of("CAN", "FRA", "JPN"), names);
 -        try (Stream<Feature> features = countries.features(false)) {
++        try (Stream<AbstractFeature> features = countries.features(false)) {
+             names = features.map(f -> 
f.getPropertyValue("native_name")).toList();
+         }
+         assertSetEquals(List.of("Canada", "France", "日本"), names);
+     }
+ 
      /**
       * Checks that operations stacked on feature stream are well executed.
       * This test focuses on mapping and peeking actions overloaded by SQL 
streams.
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
index 5f2d84d995,80dec5de00..7add475ead
--- 
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
@@@ -40,15 -45,20 +45,16 @@@ import org.apache.sis.util.CharSequence
  import org.apache.sis.util.Emptiable;
  import org.apache.sis.util.iso.Names;
  
 -// Specific to the geoapi-3.1 and geoapi-4.0 branches:
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.Attribute;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.Operation;
 -import org.opengis.filter.FilterFactory;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.InvalidFilterValueException;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.ValueReference;
 -import org.opengis.filter.SortBy;
 -import org.opengis.filter.SortProperty;
 +// Specific to the main branch:
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.AbstractAttribute;
++import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.pending.geoapi.filter.Literal;
 +import org.apache.sis.pending.geoapi.filter.ValueReference;
 +import org.apache.sis.pending.geoapi.filter.SortBy;
 +import org.apache.sis.pending.geoapi.filter.SortProperty;
  
  
  /**
@@@ -387,8 -395,9 +393,9 @@@ public class FeatureQuery extends Quer
       */
      @SafeVarargs
      @SuppressWarnings("varargs")
 -    public final void setSortBy(final SortProperty<Feature>... properties) {
 +    final void setSortBy(final SortProperty<AbstractFeature>... properties) {
+         @SuppressWarnings("LocalVariableHidesMemberVariable")
 -        SortBy<Feature> sortBy = null;
 +        SortBy<AbstractFeature> sortBy = null;
          if (properties != null) {
              sortBy = SortByComparator.create(properties);
          }
@@@ -609,6 -614,25 +616,25 @@@
              this.alias      = alias;
          }
  
+         /**
+          * Adds this named expression as a property into the given builder.
+          *
+          * @param  builder  the builder where to add the property.
+          * @return whether the property has been successfully added.
+          */
+         final boolean addTo(final FeatureProjectionBuilder builder) {
 -            final FeatureExpression<? super Feature, ?> fex = 
FeatureExpression.castOrCopy(expression);
++            final FeatureExpression<? super AbstractFeature, ?> fex = 
FeatureExpression.castOrCopy(expression);
+             if (fex != null) {
+                 final FeatureProjectionBuilder.Item item = 
fex.expectedType(builder);
+                 if (item != null) {
+                     item.setName(alias);    // Need to be invoked aven if the 
alias is null.
+                     item.setValueGetter(expression, type == 
ProjectionType.STORED);
+                     return true;
+                 }
+             }
+             return false;
+         }
+ 
          /**
           * Returns a hash code value for this column.
           *
@@@ -709,6 -733,36 +735,36 @@@
          return xpaths;
      }
  
+     /**
+      * Creates the projection (in <abbr>SQL</abbr> sense) of the given 
feature type.
+      * If some expressions have no name, default names are computed as below:
+      *
+      * <ul>
+      *   <li>If the expression is an instance of {@link ValueReference}, the 
name of the
+      *       property referenced by the {@linkplain ValueReference#getXPath() 
XPath}.</li>
+      *   <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.
+      */
 -    final Optional<FeatureProjection> project(final FeatureType sourceType, 
final Locale locale) {
++    final Optional<FeatureProjection> project(final DefaultFeatureType 
sourceType, final Locale locale) {
+         if (projection == null) {
+             return Optional.empty();
+         }
+         final var builder = new FeatureProjectionBuilder(sourceType, locale);
+         for (int column = 0; column < projection.length; column++) {
+             final NamedExpression item = projection[column];
+             if (!item.addTo(builder)) {
+                 final var name = 
item.expression.getFunctionName().toInternationalString();
 -                throw new 
InvalidFilterValueException(Resources.forLocale(locale)
++                throw new IllegalArgumentException(Resources.forLocale(locale)
+                             .getString(Resources.Keys.InvalidExpression_2, 
column, name));
+             }
+         }
+         builder.setName(sourceType.getName());
+         return builder.project();
+     }
+ 
      /**
       * Applies this query on the given feature set.
       * This method is invoked by the default implementation of {@link 
FeatureSet#subset(Query)}.
diff --cc 
endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
index 88f140538c,c4d4e792a7..9230ef5e02
--- 
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
@@@ -91,12 -91,12 +91,12 @@@ final class FeatureSubset extends Abstr
       * Returns a description of properties that are common to all features in 
this dataset.
       */
      @Override
 -    public synchronized FeatureType getType() throws DataStoreException {
 +    public synchronized DefaultFeatureType getType() throws 
DataStoreException {
          if (resultType == null) {
 -            final FeatureType type = source.getType();
 +            final DefaultFeatureType type = source.getType();
              try {
-                 projection = FeatureProjection.create(type, 
query.getProjection());
-                 resultType = (projection != null) ? projection.featureType : 
type;
+                 projection = query.project(type, 
listeners.getLocale()).orElse(null);
+                 resultType = (projection != null) ? projection.typeRequested 
: type;
              } catch (IllegalArgumentException e) {
                  throw new 
DataStoreContentException(Resources.forLocale(listeners.getLocale())
                          
.getString(Resources.Keys.CanNotDeriveTypeFromFeature_1, type.getName()), e);

Reply via email to