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

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

commit 7ea6427d63023545787e8810a526bb7382ff3f1a
Merge: e621d10 c6991bf
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sun Jan 2 02:33:49 2022 +0100

    Merge branch 'geoapi-3.1'

 .../sis/gui/coverage/ImagePropertyExplorer.java    |  54 +--
 .../dataset/{CopyAction.java => PathAction.java}   |  38 +-
 .../org/apache/sis/gui/dataset/ResourceTree.java   |   8 +-
 .../org/apache/sis/gui/metadata/MetadataTree.java  | 159 ++++++--
 .../sis/gui/metadata/StandardMetadataTree.java     |  73 ++--
 .../apache/sis/internal/gui/ExceptionReporter.java |   3 +-
 .../sis/internal/gui/PropertyValueFormatter.java   |  85 ++++
 .../org/apache/sis/internal/gui/PropertyView.java  |  55 ++-
 .../org/apache/sis/internal/gui/Resources.java     |  15 +
 .../apache/sis/internal/gui/Resources.properties   |   3 +
 .../sis/internal/gui/Resources_fr.properties       |   3 +
 .../org/apache/sis/coverage/SampleDimension.java   |  75 ++--
 .../org/apache/sis/feature/DefaultFeatureType.java |  14 +-
 .../sis/feature/builder/FeatureTypeBuilder.java    |  13 +-
 .../org/apache/sis/filter/ArithmeticFunction.java  |   8 +-
 .../org/apache/sis/filter/AssociationValue.java    | 239 +++++++++++
 .../org/apache/sis/filter/ConvertFunction.java     |  26 +-
 .../apache/sis/filter/DefaultFilterFactory.java    |   4 +-
 .../java/org/apache/sis/filter/LeafExpression.java |  14 +-
 .../java/org/apache/sis/filter/Optimization.java   |   2 +-
 .../java/org/apache/sis/filter/PropertyValue.java  | 208 ++++++----
 .../sis/internal/coverage/SampleDimensions.java    |  41 --
 .../sis/internal/coverage/j2d/Colorizer.java       |  20 +-
 .../sis/internal/feature/FeatureExpression.java    |  52 +--
 .../apache/sis/internal/feature/package-info.java  |   2 +-
 .../sis/internal/filter/GeometryConverter.java     |   8 +-
 .../sis/internal/filter/sqlmm/SpatialFunction.java |  25 +-
 .../sis/internal/filter/sqlmm/package-info.java    |   2 +-
 .../org/apache/sis/filter/LogicalFilterTest.java   |   4 +-
 .../java/org/apache/sis/filter/PeriodLiteral.java  |   2 +-
 .../org/apache/sis/util/iso/DefaultLocalName.java  |   8 +-
 .../org/apache/sis/util/iso/DefaultMemberName.java |   2 +-
 .../apache/sis/util/iso/DefaultNameFactory.java    |   4 +-
 .../org/apache/sis/util/iso/DefaultScopedName.java |   4 +-
 .../org/apache/sis/util/iso/DefaultTypeName.java   |   4 +-
 .../java/org/apache/sis/util/iso/TypeNames.java    |   2 +-
 .../apache/sis/metadata/TreeTableFormatTest.java   |   2 +-
 .../org/apache/sis/metadata/TreeTableViewTest.java |   2 +-
 .../extent/DefaultGeographicBoundingBoxTest.java   |   5 +-
 .../DefaultDataIdentificationTest.java             |   2 +-
 .../sis/internal/map/coverage/RenderingData.java   |  17 +-
 .../internal/referencing/WraparoundApplicator.java |  57 +--
 .../sis/internal/referencing/package-info.java     |   2 +-
 .../apache/sis/internal/util/PropertyFormat.java   |  37 +-
 .../java/org/apache/sis/internal/util/Strings.java |  28 +-
 .../java/org/apache/sis/internal/util/XPaths.java  |  58 ++-
 .../org/apache/sis/internal/util/XPointer.java     | 113 ++++++
 .../org/apache/sis/internal/util/package-info.java |   2 +-
 .../java/org/apache/sis/measure/UnitFormat.java    |   3 +-
 .../sis/util/collection/TreeTableFormat.java       |   2 +-
 .../java/org/apache/sis/util/resources/Errors.java |   5 +
 .../apache/sis/util/resources/Errors.properties    |   1 +
 .../apache/sis/util/resources/Errors_fr.properties |   1 +
 .../org/apache/sis/util/resources/Vocabulary.java  |  15 +
 .../sis/util/resources/Vocabulary.properties       |   3 +
 .../sis/util/resources/Vocabulary_fr.properties    |   3 +
 .../org/apache/sis/internal/util/XPathsTest.java   |  16 +-
 .../util/{XPathsTest.java => XPointerTest.java}    |  36 +-
 .../java/org/apache/sis/measure/UnitsTest.java     |   2 +-
 .../apache/sis/test/suite/UtilityTestSuite.java    |   3 +-
 .../java/org/apache/sis/storage/landsat/Band.java  |   2 +-
 .../sis/internal/geotiff/SchemaModifier.java       |  16 +-
 .../sis/internal/storage/inflater/CCITTRLE.java    |   8 +-
 .../storage/inflater/CompressionChannel.java       |   6 +-
 .../storage/inflater/HorizontalPredictor.java      | 443 +++++++++++----------
 .../sis/internal/storage/inflater/Inflater.java    |  12 +-
 .../apache/sis/internal/storage/inflater/LZW.java  |   8 +-
 .../sis/internal/storage/inflater/PackBits.java    |   8 +-
 .../internal/storage/inflater/PixelChannel.java    |   4 +-
 .../storage/inflater/PredictorChannel.java         |  11 +-
 .../apache/sis/internal/storage/inflater/ZIP.java  |   8 +-
 .../internal/storage/inflater/package-info.java    |   2 +-
 .../apache/sis/storage/geotiff/GeoTiffStore.java   |   2 +-
 .../sis/storage/geotiff/ImageFileDirectory.java    |   9 +-
 .../apache/sis/storage/geotiff/NativeMetadata.java |  36 +-
 .../java/org/apache/sis/storage/geotiff/Tags.java  |  14 +-
 .../apache/sis/storage/geotiff/XMLMetadata.java    | 289 ++++++++++++++
 .../internal/storage/inflater/CCITTRLETest.java    |   2 +-
 .../org/apache/sis/internal/netcdf/CRSBuilder.java |  47 ++-
 .../sis/internal/sql/postgis/RasterReader.java     |   6 +-
 .../org/apache/sis/storage/sql/SQLStoreTest.java   |   4 +-
 .../sis/internal/storage/TiledGridCoverage.java    |  27 +-
 .../sis/internal/storage/TiledGridResource.java    |   1 +
 .../sis/internal/storage/io/IOUtilities.java       |  23 +-
 .../java/org/apache/sis/storage/FeatureQuery.java  | 129 ++++--
 .../org/apache/sis/storage/FeatureQueryTest.java   | 177 ++++++--
 86 files changed, 2165 insertions(+), 823 deletions(-)

diff --cc 
core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
index 54c1ef3,3d0af60..4dc9e91
--- 
a/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/coverage/SampleDimension.java
@@@ -77,7 -77,10 +77,7 @@@ import org.apache.sis.util.Debug
   *
   * @author  Martin Desruisseaux (IRD, Geomatys)
   * @author  Alexis Manin (Geomatys)
-  * @version 1.1
+  * @version 1.2
 - *
 - * @see org.opengis.metadata.content.SampleDimension
 - *
   * @since 1.0
   * @module
   */
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
index fcc86ee,e2ebc82..882be37
--- 
a/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/feature/DefaultFeatureType.java
@@@ -293,9 -293,9 +293,9 @@@ public class DefaultFeatureType extend
          }
          /*
           * Before to resolve cyclic associations, verify that operations 
depend only on existing properties.
-          * Note: the 'allProperties' collection has been created by 
computeTransientFields(…) above.
+          * Note: the `allProperties` collection has been created by 
computeTransientFields(…) above.
           */
 -        for (final PropertyType property : allProperties) {
 +        for (final AbstractIdentifiedType property : allProperties) {
              if (property instanceof AbstractOperation) {
                  for (final String dependency : ((AbstractOperation) 
property).getDependencies()) {
                      if (!byName.containsKey(dependency)) {
@@@ -392,10 -392,10 +392,10 @@@
           * name for convenience, provided that it does not create ambiguity.  
If a short alias could map to two or
           * more properties, then that alias is not added.
           *
-          * In the 'aliases' map below, null values will be assigned to 
ambiguous short names.
+          * In the `aliases` map below, null values will be assigned to 
ambiguous short names.
           */
 -        final Map<String, PropertyType> aliases = new LinkedHashMap<>();
 -        for (final PropertyType property : allProperties) {
 +        final Map<String, AbstractIdentifiedType> aliases = new 
LinkedHashMap<>();
 +        for (final AbstractIdentifiedType property : allProperties) {
              GenericName name = property.getName();
              while (name instanceof ScopedName) {
                  if (name == (name = ((ScopedName) name).tail())) break;   // 
Safety against broken implementations.
@@@ -527,12 -521,19 +527,14 @@@
       * @param  previous  previous results, for avoiding never ending loop.
       * @return {@code true} if all names have been resolved.
       */
 -    private boolean resolve(final FeatureType feature, final 
Map<FeatureType,Boolean> previous) {
 +    private boolean resolve(final DefaultFeatureType feature, final 
Map<FeatureType,Boolean> previous) {
          /*
           * The isResolved field is used only as a cache for skipping 
completely the DefaultFeatureType instance if
-          * we have determined that there is no unresolved name.
+          * we have determined that there is no unresolved name.  If the given 
argument is not a DefaultFeatureType
+          * instance, conservatively assumes `isSimple`. It may cause more 
calculation than needed, but should not
+          * change the result.
           */
 -        if (feature instanceof DefaultFeatureType) {
 -            final DefaultFeatureType dt = (DefaultFeatureType) feature;
 -            return dt.isResolved = resolve(dt, dt.properties, previous, 
dt.isResolved);
 -        } else {
 -            return resolve(feature, feature.getProperties(false), previous, 
feature.isSimple());
 -        }
 +        return feature.isResolved = resolve(feature, feature.properties, 
previous, feature.isResolved);
      }
  
      /**
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java
index de92b37,3ab4bdc..41bc198
--- 
a/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/feature/builder/FeatureTypeBuilder.java
@@@ -697,11 -692,9 +697,13 @@@ public class FeatureTypeBuilder extend
  
      /**
       * Creates a new {@code AttributeType} builder initialized to the same 
characteristics than the given template.
+      * If the new attribute duplicates an existing one (for example if the 
same template is used many times),
+      * caller should use the returned builder for modifying some attributes.
       *
 +     * <div class="warning"><b>Warning:</b>
 +     * The {@code template} argument type will be changed to {@code 
AttributeType} if and when such interface
 +     * will be defined in GeoAPI.</div>
 +     *
       * @param  <V>       the compile-time type of values in the {@code 
template} argument.
       * @param  template  an existing attribute type to use as a template.
       * @return a builder for an {@code AttributeType}, initialized with the 
values of the given template.
@@@ -808,12 -797,10 +810,14 @@@
  
      /**
       * Creates a new {@code FeatureAssociationRole} builder initialized to 
the same characteristics
-      * than the given template.
+      * than the given template. If the new association duplicates an existing 
one (for example if the
+      * same template is used many times), caller should use the returned 
builder for modifying some
+      * associations.
       *
 +     * <div class="warning"><b>Warning:</b>
 +     * The {@code template} argument type will be changed to {@code 
FeatureAssociationRole} if and when such interface
 +     * will be defined in GeoAPI.</div>
 +     *
       * @param  template  an existing feature association to use as a template.
       * @return a builder for an {@code FeatureAssociationRole}, initialized 
with the values of the given template.
       *
@@@ -830,18 -817,20 +834,23 @@@
      /**
       * Adds the given property in the feature type properties.
       * The given property shall be an instance of one of the following types:
+      *
       * <ul>
 -     *   <li>{@link AttributeType}, in which case this method delegate to 
{@link #addAttribute(AttributeType)}.</li>
 -     *   <li>{@link FeatureAssociationRole}, in which case this method 
delegate to {@link #addAssociation(FeatureAssociationRole)}.</li>
 -     *   <li>{@link Operation}, in which case the given operation object will 
be added verbatim in the {@code FeatureType};
 +     *   <li>{@code AttributeType}, in which case this method delegate to 
{@code addAttribute(AttributeType)}.</li>
 +     *   <li>{@code FeatureAssociationRole}, in which case this method 
delegate to {@code addAssociation(FeatureAssociationRole)}.</li>
 +     *   <li>{@code Operation}, in which case the given operation object will 
be added verbatim in the {@code FeatureType};
       *       this builder does not create new operations.</li>
       * </ul>
       *
+      * This method does not verify if the given property duplicates an 
existing property.
+      * If the same template is used many times, then the caller should use 
the returned builder
+      * for modifying some properties.
+      *
 +     * <div class="warning"><b>Warning:</b> In a future SIS version, the 
argument type may be changed to the
 +     * {@code org.opengis.feature.PropertyType} interface. This change is 
pending GeoAPI revision.</div>
 +     *
       * @param  template  the property to add to the feature type.
-      * @return a builder initialized to the given builder.
+      * @return a builder initialized to the given template.
       *         In the {@code Operation} case, the builder is a read-only 
accessor on the operation properties.
       *
       * @see #properties()
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java
index 0000000,a46b3bb..30c1a7a
mode 000000,100644..100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java
@@@ -1,0 -1,234 +1,239 @@@
+ /*
+  * 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;
+ 
+ import java.util.Arrays;
+ import java.util.List;
+ import java.util.Collection;
+ import java.util.Collections;
+ import java.util.Optional;
+ import java.util.StringJoiner;
+ import org.apache.sis.feature.Features;
+ import org.apache.sis.feature.builder.FeatureTypeBuilder;
+ import org.apache.sis.feature.builder.PropertyTypeBuilder;
+ 
+ // Branch-dependent imports
 -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;
++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.internal.geoapi.filter.Name;
++import org.apache.sis.internal.geoapi.filter.ValueReference;
+ 
+ 
+ /**
+  * Expression whose value is computed by retrieving the value indicated by 
the provided path.
+  * This is used for value reference given by x-path such as "a/b/c". The last 
element of the path
+  * (the tip) is evaluated by a {@link PropertyValue}.
+  *
+  * @author  Martin Desruisseaux (Geomatys)
+  * @version 1.2
+  *
+  * @param  <V>  the type of value computed by the expression.
+  *
+  * @see PropertyValue
+  *
+  * @since 1.2
+  * @module
+  */
 -final class AssociationValue<V> extends LeafExpression<Feature, V>
 -        implements ValueReference<Feature, V>, 
Optimization.OnExpression<Feature, V>
++final class AssociationValue<V> extends LeafExpression<AbstractFeature, V>
++        implements ValueReference<AbstractFeature, V>, 
Optimization.OnExpression<AbstractFeature, V>
+ {
+     /**
+      * Path to the property from which to retrieve the value.
 -     * Each element in the array is an argument to give in a call to {@link 
Feature#getProperty(String)}.
++     * Each element in the array is an argument to give in a call to {@code 
Feature.getProperty(String)}.
+      * This array should be considered read-only because it may be shared.
+      */
+     private final String[] path;
+ 
+     /**
+      * Expression to use for evaluating the property value after the last 
element of the path.
+      */
+     private final PropertyValue<V> accessor;
+ 
+     /**
+      * Creates a new expression retrieving values from a property at the 
given path.
+      *
+      * @param  path      components of the path before the property evaluated 
by {@code accessor}.
+      * @param  accessor  expression to use for evaluating the property value 
after the last element of the path.
+      */
+     AssociationValue(final List<String> path, final PropertyValue<V> 
accessor) {
+         this.path = path.toArray(new String[path.size()]);
+         this.accessor = accessor;
+     }
+ 
+     /**
+      * Creates a new expression retrieving values from a property at the 
given path.
+      * This constructor is used for creating new expression with the same 
path than
+      * a previous expression but a different accessor.
+      *
+      * @param  path      components of the path, not cloned (we share arrays).
+      * @param  accessor  expression to use for evaluating the property value 
after the last element of the path.
+      */
+     private AssociationValue(final String[] path, final PropertyValue<V> 
accessor) {
+         this.path = path;
+         this.accessor = accessor;
+     }
+ 
++    @Override
++    public final ScopedName getFunctionName() {
++        return Name.VALUE_REFERENCE;
++    }
++
+     /**
+      * For {@link #toString()} implementation.
+      */
+     @Override
+     protected final Collection<?> getChildren() {
+         return Collections.singleton(getXPath());
+     }
+ 
+     /**
+      * Returns the name of the property whose value will be returned by the 
{@link #apply(Object)} method.
+      */
+     @Override
+     public final String getXPath() {
+         final StringJoiner sb = new StringJoiner("/", accessor.isVirtual ? 
PropertyValue.VIRTUAL_PREFIX : "", "");
+         for (final String p : path) sb.add(p);
+         return sb.add(accessor.name).toString();
+     }
+ 
+     /**
+      * Returns the value of the property at the path given at construction 
time.
+      * Path components should be feature associations. If this is not the 
case,
+      * this method silently returns {@code null}.
+      *
+      * @param  feature  the feature from which to get a value, or {@code 
null}.
+      * @return value for the property identified by the XPath (may be {@code 
null}).
+      */
+     @Override
 -    public V apply(Feature instance) {
++    public V apply(AbstractFeature instance) {
+ walk:   if (instance != null) {
+             for (final String p : path) {
+                 final Object value = instance.getPropertyValue(p);
 -                if (!(value instanceof Feature)) break walk;
 -                instance = (Feature) value;
++                if (!(value instanceof AbstractFeature)) break walk;
++                instance = (AbstractFeature) value;
+             }
+             return accessor.apply(instance);
+         }
+         return null;
+     }
+ 
+     /**
+      * If at least one evaluated property is a link, replaces this expression 
by more direct references
+      * to the target properties. This is needed for better SQL WHERE clause 
in database queries.
+      */
+     @Override
 -    public Expression<Feature, V> optimize(final Optimization optimization) {
 -        final FeatureType specifiedType = optimization.getFeatureType();
++    public Expression<AbstractFeature, V> optimize(final Optimization 
optimization) {
++        final DefaultFeatureType specifiedType = 
optimization.getFeatureType();
+ walk:   if (specifiedType != null) try {
 -            FeatureType type = specifiedType;
++            DefaultFeatureType type = specifiedType;
+             String[] direct = path;                 // To be cloned before 
any modification.
+             for (int i=0; i<path.length; i++) {
 -                PropertyType property = type.getProperty(path[i]);
++                AbstractIdentifiedType property = type.getProperty(path[i]);
+                 Optional<String> link = Features.getLinkTarget(property);
+                 if (link.isPresent()) {
+                     if (direct == path) direct = direct.clone();
+                     property = type.getProperty(direct[i] = link.get());
+                 }
 -                if (!(property instanceof FeatureAssociationRole)) break walk;
 -                type = ((FeatureAssociationRole) property).getValueType();
++                if (!(property instanceof DefaultAssociationRole)) break walk;
++                type = ((DefaultAssociationRole) property).getValueType();
+             }
+             /*
+              * At this point all links have been resolved, up to the final 
property to evaluate.
+              * Delegate the final property optimization to `accessor` which 
may not only resolve
+              * links but also tune the `ObjectConverter`.
+              */
+             final PropertyValue<V> converted;
+             optimization.setFeatureType(type);
+             try {
+                 converted = accessor.optimize(optimization);
+             } finally {
+                 optimization.setFeatureType(specifiedType);
+             }
+             if (converted != accessor || direct != path) {
+                 return new AssociationValue<>(direct, converted);
+             }
 -        } catch (PropertyNotFoundException e) {
++        } catch (IllegalArgumentException e) {
+             warning(e, true);
+         }
+         return this;
+     }
+ 
+     /**
+      * Returns an expression that provides values as instances of the 
specified class.
+      */
+     @Override
+     @SuppressWarnings("unchecked")
 -    public final <N> Expression<Feature,N> toValueType(final Class<N> target) 
{
++    public final <N> Expression<AbstractFeature,N> toValueType(final Class<N> 
target) {
+         final PropertyValue<N> converted = accessor.toValueType(target);
+         if (converted == accessor) {
+             return (AssociationValue<N>) this;
+         }
+         return new AssociationValue<>(path, converted);
+     }
+ 
+     /**
+      * Provides the expected type of values produced by this expression when 
a feature of the given type is evaluated.
+      *
+      * @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.
+      * @return builder of the added property, or {@code null} if this method 
can not add a property.
+      * @throws IllegalArgumentException if this method can not determine the 
property type for the given feature type.
+      */
+     @Override
 -    public PropertyTypeBuilder expectedType(FeatureType valueType, final 
FeatureTypeBuilder addTo) {
++    public PropertyTypeBuilder expectedType(DefaultFeatureType valueType, 
final FeatureTypeBuilder addTo) {
+         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);
+                 }
+                 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);
+     }
+ 
+     /**
+      * Returns a hash code value for this association.
+      */
+     @Override
+     public int hashCode() {
+         return Arrays.hashCode(path) + accessor.hashCode();
+     }
+ 
+     /**
+      * Compares this value reference with the given object for equality.
+      */
+     @Override
+     public boolean equals(final Object obj) {
+         if (obj instanceof AssociationValue<?>) {
+             final AssociationValue<?> other = (AssociationValue<?>) obj;
+             return Arrays.equals(path, other.path) && 
accessor.equals(other.accessor);
+         }
+         return false;
+     }
+ }
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/filter/ConvertFunction.java
index bfb8017,90adf3a..948f813
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/ConvertFunction.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ConvertFunction.java
@@@ -36,9 -37,9 +36,9 @@@ import org.apache.sis.feature.DefaultFe
   * Expression whose results are converted to a different type.
   *
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.2
   *
 - * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <S>  the type of value computed by the wrapped exception. This is 
the type to convert.
   * @param  <V>  the type of value computed by this expression. This is the 
type after conversion.
   *
@@@ -156,12 -157,16 +156,16 @@@ final class ConvertFunction<R,S,V> exte
      }
  
      /**
-      * Provides the expected type of values produced by this expression
-      * when a feature of the given type is evaluated.
+      * Provides the type of values produced by this expression when a feature 
of the given type is evaluated.
+      * May return {@code null} if the type can not be determined.
       */
      @Override
 -    public PropertyTypeBuilder expectedType(final FeatureType valueType, 
final FeatureTypeBuilder addTo) {
 +    public PropertyTypeBuilder expectedType(final DefaultFeatureType 
valueType, final FeatureTypeBuilder addTo) {
-         final PropertyTypeBuilder p = 
FeatureExpression.expectedType(expression, valueType, addTo);
+         final FeatureExpression<?,?> fex = 
FeatureExpression.castOrCopy(expression);
+         if (fex == null) {
+             return null;
+         }
+         final PropertyTypeBuilder p = fex.expectedType(valueType, addTo);
          if (p instanceof AttributeTypeBuilder<?>) {
              return ((AttributeTypeBuilder<?>) 
p).setValueClass(getValueClass());
          }
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
index feffd85,158c2f9..7feb03e
--- 
a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
@@@ -55,9 -48,9 +55,9 @@@ import org.apache.sis.internal.geoapi.f
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.2
   *
 - * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) to use as inputs.
 + * @param  <R>  the type of resources (e.g. {@link AbstractFeature}) to use 
as inputs.
   * @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
   *              {@link org.locationtech.jts.geom.Geometry} or ESRI {@link 
com.esri.core.geometry.Geometry}.
@@@ -199,7 -226,9 +199,9 @@@ public abstract class DefaultFilterFact
           * @return an expression evaluating the referenced property value.
           */
          @Override
 -        public <V> ValueReference<Feature,V> property(final String xpath, 
final Class<V> type) {
 +        public <V> Expression<AbstractFeature,V> property(final String xpath, 
final Class<V> type) {
+             ArgumentChecks.ensureNonEmpty("xpath", xpath);
+             ArgumentChecks.ensureNonNull ("type",  type);
              return PropertyValue.create(xpath, type);
          }
      }
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
index 057e829,80e5bc7..4a238a9
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
@@@ -27,16 -28,18 +28,18 @@@ import org.apache.sis.util.Unconvertibl
  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.util.resources.Errors;
+ import org.apache.sis.internal.util.XPaths;
  
  // Branch-dependent imports
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.PropertyType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.feature.IdentifiedType;
 -import org.opengis.feature.Operation;
 -import org.opengis.feature.PropertyNotFoundException;
 -import org.opengis.filter.ValueReference;
 +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.internal.geoapi.filter.Name;
 +import org.apache.sis.internal.geoapi.filter.ValueReference;
  
  
  /**
@@@ -53,7 -58,9 +58,9 @@@
   * @since 1.1
   * @module
   */
- abstract class PropertyValue<V> extends LeafExpression<AbstractFeature,V> 
implements ValueReference<AbstractFeature,V> {
 -abstract class PropertyValue<V> extends LeafExpression<Feature,V>
 -        implements ValueReference<Feature,V>, 
Optimization.OnExpression<Feature,V>
++abstract class PropertyValue<V> extends LeafExpression<AbstractFeature,V>
++        implements ValueReference<AbstractFeature,V>, 
Optimization.OnExpression<AbstractFeature,V>
+ {
      /**
       * For cross-version compatibility.
       */
@@@ -65,34 -73,68 +73,73 @@@
      protected final String name;
  
      /**
+      * Whether the property to fetch is considered virtual (a property that 
may be defined only in sub-types).
+      * If {@code true}, then {@link #expectedType(FeatureType, 
FeatureTypeBuilder)} will not throw an exception
+      * if the property is not found.
+      */
+     protected final boolean isVirtual;
+ 
+     /**
+      * The prefix in a x-path for considering a property as virual.
+      */
+     static final String VIRTUAL_PREFIX = "/*/";
+ 
+     /**
       * Creates a new expression retrieving values from a property of the 
given name.
       */
-     protected PropertyValue(final String name) {
-         ArgumentChecks.ensureNonNull("name", name);
+     protected PropertyValue(final String name, final boolean isVirtual) {
          this.name = name;
+         this.isVirtual = isVirtual;
      }
  
 +    @Override
 +    public final ScopedName getFunctionName() {
 +        return Name.VALUE_REFERENCE;
 +    }
 +
      /**
-      * Creates a new expression retrieving values from a property of the 
given name.
+      * Creates a new expression retrieving values from a property of the 
given path.
+      * Simple path expressions of the form "a/b/c" can be used.
       *
-      * @param  <V>   compile-time value of {@code type}.
-      * @param  name  the name of the property to fetch.
-      * @param  type  the desired type for the expression result.
+      * @param  <V>    compile-time value of {@code type}.
+      * @param  xpath  path (usually a single name) of the property to fetch.
+      * @param  type   the desired type for the expression result.
       * @return expression retrieving values from a property of the given name.
+      * @throws IllegalArgumentException if the given XPath is not supported.
       */
      @SuppressWarnings("unchecked")
-     static <V> PropertyValue<V> create(final String name, final Class<V> 
type) {
-         ArgumentChecks.ensureNonNull("type", type);
-         if (type == Object.class) {
-             return (PropertyValue<V>) new AsObject(name);
 -    static <V> ValueReference<Feature,V> create(String xpath, final Class<V> 
type) {
++    static <V> ValueReference<AbstractFeature,V> create(String xpath, final 
Class<V> type) {
+         boolean isVirtual = false;
+         List<String> path = XPaths.split(xpath);
+ split:  if (path != null) {
+             /*
+              * If the XPath is like "/∗/property" where the root "/" is the 
feature instance,
+              * we interpret that as meaning "property of a feature of any 
type", which means
+              * to relax the restriction about the set of allowed properties.
+              */
+             final String head = path.get(0);                // List and items 
in the list are guaranteed non-empty.
+             isVirtual = head.equals("/*");
+             if (isVirtual || head.charAt(0) != XPaths.SEPARATOR) {
+                 final int offset = isVirtual ? 1 : 0;       // Skip the "/*/" 
component at index 0.
+                 final int last = path.size() - 1;
+                 if (last >= offset) {
+                     xpath = path.get(last);
+                     path  = path.subList(offset, last);
+                     break split;                            // Accept the 
path as valid.
+                 }
+             }
+             throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedXPath_1, xpath));
+         }
+         /*
+          * At this point, `xpath` is the tip of the path (i.e. prefixes have 
been removed).
+          */
+         final PropertyValue<V> tip;
+         if (type != Object.class) {
+             tip = new Converted<>(type, xpath, isVirtual);
          } else {
-             return new Converted<>(type, name);
+             tip = (PropertyValue<V>) new AsObject(xpath, isVirtual);
          }
+         return (path == null || path.isEmpty()) ? tip : new 
AssociationValue<>(path, tip);
      }
  
      /**
@@@ -112,8 -154,10 +159,10 @@@
      }
  
      /**
 -     * Returns the type of values fetched from {@link Feature} instance.
 +     * Returns the type of values fetched from {@link AbstractFeature} 
instance.
       * This is the type before conversion to the {@linkplain #getValueClass() 
target type}.
+      * The type is always {@link Object} on newly created expression because 
the type of feature property
+      * values is unknown, but may become a specialized type after {@link 
Optimization} has been applied.
       */
      protected Class<?> getSourceClass() {
          return Object.class;
@@@ -177,11 -237,12 +242,12 @@@
           * using or not the database index.
           */
          @Override
-         public Expression<AbstractFeature,?> optimize(final Optimization 
optimization) {
+         public PropertyValue<Object> optimize(final Optimization 
optimization) {
 -            final FeatureType type = optimization.getFeatureType();
 +            final DefaultFeatureType type = optimization.getFeatureType();
              if (type != null) try {
-                 return 
Features.getLinkTarget(type.getProperty(name)).map(AsObject::new).orElse(this);
+                 return Features.getLinkTarget(type.getProperty(name))
+                         .map((rename) -> new AsObject(rename, 
isVirtual)).orElse(this);
 -            } catch (PropertyNotFoundException e) {
 +            } catch (IllegalArgumentException e) {
                  warning(e, true);
              }
              return this;
@@@ -252,29 -305,42 +310,42 @@@
           * then a specialized expression is returned. Otherwise this method 
returns {@code this}.
           */
          @Override
-         public final Expression<AbstractFeature, ? extends V> optimize(final 
Optimization optimization) {
+         public final PropertyValue<V> optimize(final Optimization 
optimization) {
 -            final FeatureType featureType = optimization.getFeatureType();
 +            final DefaultFeatureType featureType = 
optimization.getFeatureType();
              if (featureType != null) try {
-                 String targetName = name;
-                 AbstractIdentifiedType property = 
featureType.getProperty(targetName);
+                 /*
+                  * Resolve link (e.g. "sis:identifier" as a reference to the 
real identifier property).
+                  * This is important for allowing `SQLStore` to use the 
property in SQL WHERE statements.
+                  * If there is no renaming to apply (which is the usual 
case), then `rename` is null.
+                  */
+                 String rename = name;
 -                PropertyType property = featureType.getProperty(rename);
++                AbstractIdentifiedType property = 
featureType.getProperty(rename);
                  Optional<String> target = Features.getLinkTarget(property);
                  if (target.isPresent()) try {
-                     targetName = target.get();
-                     property = featureType.getProperty(targetName);
+                     rename = target.get();
+                     property = featureType.getProperty(rename);
 -                } catch (PropertyNotFoundException e) {
 +                } catch (IllegalArgumentException e) {
-                     targetName = name;
                      warning(e, true);
+                     rename = name;
                  }
+                 /*
+                  * At this point we did our best effort for having the 
property as an attribute,
+                  * which allows us to get the expected type. If the type is 
not `Object`, we can
+                  * try to fetch a more specific converter than the default 
`Converted` one.
+                  */
+                 Class<?> source = getSourceClass();
+                 final Class<?> original = source;
 -                if (property instanceof AttributeType<?>) {
 -                    source = ((AttributeType<?>) property).getValueClass();
 +                if (property instanceof DefaultAttributeType<?>) {
-                     final Class<?> source = ((DefaultAttributeType<?>) 
property).getValueClass();
-                     if (source != null && source != Object.class && 
!source.isAssignableFrom(getSourceClass())) {
-                         return new CastedAndConverted<>(source, type, 
targetName);
-                     }
++                    source = ((DefaultAttributeType<?>) 
property).getValueClass();
                  }
-                 if (!targetName.equals(name)) {
-                     return rename(targetName);
+                 if (!(rename.equals(name) && source.equals(original))) {
+                     if (source == Object.class) {
+                         return new Converted<>(type, rename, isVirtual);
+                     } else {
+                         return new CastedAndConverted<>(source, type, rename, 
isVirtual);
+                     }
                  }
 -            } catch (PropertyNotFoundException e) {
 +            } catch (IllegalArgumentException e) {
                  warning(e, true);
              }
              return this;
@@@ -306,14 -372,23 +377,23 @@@
       * @throws IllegalArgumentException if this method can not determine the 
property type for the given feature type.
       */
      @Override
 -    public PropertyTypeBuilder expectedType(final FeatureType valueType, 
final FeatureTypeBuilder addTo) {
 -        PropertyType type;
 +    public PropertyTypeBuilder expectedType(final DefaultFeatureType 
valueType, final FeatureTypeBuilder addTo) {
-         AbstractIdentifiedType type = valueType.getProperty(name);        // 
May throw IllegalArgumentException.
++        AbstractIdentifiedType type;
+         try {
+             type = valueType.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);
+             }
+             throw e;
+         }
 -        while (type instanceof Operation) {
 -            final IdentifiedType result = ((Operation) type).getResult();
 -            if (result != type && result instanceof PropertyType) {
 -                type = (PropertyType) result;
 -            } else if (result instanceof FeatureType) {
 -                return addTo.addAssociation((FeatureType) 
result).setName(name);
 +        while (type instanceof AbstractOperation) {
 +            final AbstractIdentifiedType result = ((AbstractOperation) 
type).getResult();
-             if (result != type) {
-                 type = result;
++            if (result != type && result instanceof AbstractIdentifiedType) {
++                type = (AbstractIdentifiedType) result;
 +            } else if (result instanceof DefaultFeatureType) {
 +                return addTo.addAssociation((DefaultFeatureType) 
result).setName(name);
              } else {
                  return null;
              }
@@@ -346,24 -421,8 +426,8 @@@
              converter = ObjectConverters.find(source, type);
          }
  
-         /** Creates a new expression derived from an existing one except for 
the target name. */
-         private CastedAndConverted(final CastedAndConverted<S,V> other, final 
String name) {
-             super(other.type, name);
-             source = other.source;
-             converter = other.converter;
-         }
- 
-         /**
-          * Creates a new {@code CastedAndConverted} fetching values for a 
property of different name.
-          * The given name should be the target of a link that the caller has 
resolved.
-          */
-         @Override
-         protected Converted<V> rename(final String target) {
-             return new CastedAndConverted<>(this, target);
-         }
- 
          /**
 -         * Returns the type of values fetched from {@link Feature} instance.
 +         * Returns the type of values fetched from {@link AbstractFeature} 
instance.
           */
          @Override
          protected Class<S> getSourceClass() {
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
index 7633ee4,f1572b8..6a1ba54
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
@@@ -16,13 -16,15 +16,14 @@@
   */
  package org.apache.sis.internal.feature;
  
- import org.apache.sis.internal.util.CollectionsExt;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.feature.AttributeType;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.ValueReference;
++import org.apache.sis.filter.Expression;
+ 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.DefaultFeatureType;
- import org.apache.sis.feature.AbstractIdentifiedType;
- import org.apache.sis.filter.Expression;
++import org.apache.sis.internal.geoapi.filter.Literal;
++import org.apache.sis.internal.geoapi.filter.ValueReference;
  
  
  /**
@@@ -34,9 -36,9 +35,9 @@@
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.2
   *
 - * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <V>  the type of values computed by the expression.
   *
   * @since 1.0
@@@ -64,20 -66,19 +65,19 @@@ public interface FeatureExpression<R,V
       * @throws IllegalArgumentException if this method can operate only on 
some feature types
       *         and the given type is not one of them.
       */
 -    PropertyTypeBuilder expectedType(FeatureType valueType, 
FeatureTypeBuilder addTo);
 +    PropertyTypeBuilder expectedType(DefaultFeatureType valueType, 
FeatureTypeBuilder addTo);
  
      /**
-      * Provides the type of results computed by the given expression.
-      * This method executes the first of the following choices that apply:
+      * Tries to cast or convert the given expression to a {@link 
FeatureExpression}.
+      * If the given expression can not be casted, then this method creates a 
copy
+      * provided that the expression is one of the following type:
       *
       * <ol>
-      *   <li>If the expression implements {@link FeatureExpression}, delegate 
to {@link #expectedType(FeatureType,
-      *       FeatureTypeBuilder)}. Note that the invoked method may throw an 
{@link IllegalArgumentException}.</li>
-      *   <li>Otherwise if the given feature type contains exactly one 
property (including inherited properties),
-      *       adds that property to the given builder.</li>
-      *   <li>Otherwise returns {@code null}.</li>
+      *   <li>{@link Literal}.</li>
+      *   <li>{@link ValueReference}, assuming that the expression expects 
feature instances.</li>
       * </ol>
       *
+      * Otherwise this method returns {@code null}.
       * It is caller's responsibility to verify if this method returns {@code 
null} and to throw an exception in such case.
       * We leave that responsibility to the caller because (s)he may be able 
to provide better error messages.
       *
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java
index 582a18f,d250e5c..b9af211
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/SpatialFunction.java
@@@ -41,9 -42,9 +41,9 @@@ import org.apache.sis.filter.Expression
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.2
   *
 - * @param  <R>  the type of resources (e.g. {@link 
org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   *
   * @since 1.1
   * @module
@@@ -206,19 -207,22 +206,22 @@@ abstract class SpatialFunction<R> exten
       *         It may be because that expression is backed by an unsupported 
implementation.
       */
      @Override
 -    public PropertyTypeBuilder expectedType(final FeatureType valueType, 
final FeatureTypeBuilder addTo) {
 +    public PropertyTypeBuilder expectedType(final DefaultFeatureType 
valueType, final FeatureTypeBuilder addTo) {
          AttributeTypeBuilder<?> att;
  cases:  if (operation.isGeometryInOut()) {
-             final PropertyTypeBuilder type = 
FeatureExpression.expectedType(getParameters().get(0), valueType, addTo);
-             if (type instanceof AttributeTypeBuilder<?>) {
-                 att = (AttributeTypeBuilder<?>) type;
-                 final Geometries<?> library = 
Geometries.implementation(att.getValueClass());
-                 if (library != null) {
-                     att = att.setValueClass(operation.getReturnType(library));
-                     break cases;
+             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.implementation(att.getValueClass());
+                     if (library != null) {
+                         att = 
att.setValueClass(operation.getReturnType(library));
+                         break cases;
+                     }
                  }
              }
 -            throw new 
InvalidFilterValueException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression));
 +            throw new 
IllegalArgumentException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression));
          } else {
              att = addTo.addAttribute(getValueClass());
          }
diff --cc 
core/sis-feature/src/test/java/org/apache/sis/filter/PeriodLiteral.java
index d1f8e71,0a9dd86..d2213c1
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/PeriodLiteral.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/PeriodLiteral.java
@@@ -61,7 -74,11 +61,7 @@@ final strictfp class PeriodLiteral impl
      }
  
      /** Not needed for the tests. */
-     @Override public <N> Expression<AbstractFeature,N> toValueType(Class<N> 
type) {throw new UnsupportedOperationException();}
 -    @Override public ReferenceIdentifier getName()                           
{throw new UnsupportedOperationException();}
 -    @Override public RelativePosition relativePosition(TemporalPrimitive o)  
{throw new UnsupportedOperationException();}
 -    @Override public Duration         distance(TemporalGeometricPrimitive o) 
{throw new UnsupportedOperationException();}
 -    @Override public Duration         length()                               
{throw new UnsupportedOperationException();}
 -    @Override public <N> Expression<Feature,N> toValueType(Class<N> target)  
{throw new UnsupportedOperationException();}
++    @Override public <N> Expression<AbstractFeature,N> toValueType(Class<N> 
target) {throw new UnsupportedOperationException();}
  
      /**
       * Hash code value. Used by the tests for checking the results of 
deserialization.
diff --cc 
core/sis-metadata/src/test/java/org/apache/sis/metadata/TreeTableFormatTest.java
index eaf3bae,9571ad9..67c4165
--- 
a/core/sis-metadata/src/test/java/org/apache/sis/metadata/TreeTableFormatTest.java
+++ 
b/core/sis-metadata/src/test/java/org/apache/sis/metadata/TreeTableFormatTest.java
@@@ -97,8 -96,9 +97,8 @@@ public final strictfp class TreeTableFo
              "  │   │       ├─East bound longitude…… 180°E\n" +
              "  │   │       ├─South bound latitude…… 90°S\n" +
              "  │   │       ├─North bound latitude…… 90°N\n" +
-             "  │   │       └─Extent type code……………… true\n" +
+             "  │   │       └─Extent type code……………… True\n" +
 -            "  │   ├─Organisation……………………………………………… Kōdansha\n" +
 -            "  │   └─Role…………………………………………………………………… Editor\n" +
 +            "  │   └─Party………………………………………………………………… Kōdansha\n" +
              "  ├─Presentation form (1 of 2)…………………… Document digital\n" +
              "  ├─Presentation form (2 of 2)…………………… Document hardcopy\n" +
              "  └─ISBN……………………………………………………………………………… 9782505004509\n", text);
diff --cc 
core/sis-utility/src/main/java/org/apache/sis/internal/util/PropertyFormat.java
index a30a890,3fbcecf..47f7224
--- 
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/PropertyFormat.java
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/PropertyFormat.java
@@@ -92,13 -97,15 +97,15 @@@ public abstract class PropertyFormat ex
              }
              text = columnFormat.format(value);
          } else if (value instanceof InternationalString) {
-             text = ((InternationalString) value).toString(getLocale());
+             text = freeText(((InternationalString) 
value).toString(getLocale()));
          } else if (value instanceof CharSequence) {
-             text = value.toString();
+             text = freeText(value.toString());
 -        } else if (value instanceof ControlledVocabulary) {
 -            text = 
MetadataServices.getInstance().getCodeTitle((ControlledVocabulary) value, 
getLocale());
 -        } else if (value instanceof Boolean) {
 -            text = Vocabulary.getResources(getLocale()).getString((Boolean) 
value ? Vocabulary.Keys.True : Vocabulary.Keys.False);
 +        } else if (value instanceof CodeList<?>) {
 +            text = MetadataServices.getInstance().getCodeTitle((CodeList<?>) 
value, getLocale());
          } else if (value instanceof Enum<?>) {
              text = CharSequences.upperCaseToSentence(((Enum<?>) 
value).name());
++        } else if (value instanceof Boolean) {
++            text = Vocabulary.getResources(getLocale()).getString((Boolean) 
value ? Vocabulary.Keys.True : Vocabulary.Keys.False);
          } else if (value instanceof Type) {
              appendName(((Type) value).getTypeName());
              return;
diff --cc 
storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
index e1bfe7d,ee2f90b..77fe210
--- 
a/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
+++ 
b/storage/sis-netcdf/src/main/java/org/apache/sis/internal/netcdf/CRSBuilder.java
@@@ -973,10 -975,11 +979,11 @@@ previous:   for (int i=components.size(
           */
          @Override void createCS(CSFactory factory, Map<String,?> properties, 
CoordinateSystemAxis[] axes) throws FactoryException {
              try {
-                 if (axes.length > 2) {
-                     coordinateSystem = factory.createAffineCS(properties, 
axes[0], axes[1], axes[2]);
-                 } else {
-                     coordinateSystem = factory.createAffineCS(properties, 
axes[0], axes[1]);
+                 switch (axes.length) {
+                     case 0:  break;     // Should never happen but we are 
paranoiac.
 -                    case 1:  coordinateSystem = 
factory.createParametricCS(properties, axes[0]); return;
++                    case 1:  coordinateSystem = new 
org.apache.sis.referencing.cs.DefaultParametricCS(properties, axes[0]); return;
+                     case 2:  coordinateSystem = 
factory.createAffineCS(properties, axes[0], axes[1]); return;
+                     default: coordinateSystem = 
factory.createAffineCS(properties, axes[0], axes[1], axes[2]); return;
                  }
              } catch (InvalidGeodeticParameterException e) {
                  /*
diff --cc 
storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
index d5d274e,5756e44..c6603b3
--- 
a/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
+++ 
b/storage/sis-sqlstore/src/test/java/org/apache/sis/storage/sql/SQLStoreTest.java
@@@ -378,15 -380,17 +378,15 @@@ public final strictfp class SQLStoreTes
           */
          final FeatureSet   parks = dataset.findResource("Parks");
          final FeatureQuery query = new FeatureQuery();
-         query.setProjection(new 
FeatureQuery.NamedExpression(FF.property(desiredProperty)));
+         query.setProjection(FF.property(desiredProperty));
 -        query.setSortBy(FF.sort(FF.property("country"),       
SortOrder.DESCENDING),
 -                        FF.sort(FF.property(desiredProperty), 
SortOrder.ASCENDING));
          final FeatureSet subset = parks.subset(query);
          /*
 -         * Verify that all features have the expected property, then verify 
the sorted values.
 +         * Verify that all features have the expected property.
           */
          final Object[] values;
 -        try (Stream<Feature> features = subset.features(false)) {
 +        try (Stream<AbstractFeature> features = subset.features(false)) {
              values = features.map(f -> {
 -                final PropertyType p = 
TestUtilities.getSingleton(f.getType().getProperties(true));
 +                final AbstractIdentifiedType p = 
TestUtilities.getSingleton(f.getType().getProperties(true));
                  assertEquals("Feature has wrong property.", desiredProperty, 
p.getName().toString());
                  return f.getPropertyValue(desiredProperty);
              }).toArray();
@@@ -535,8 -539,9 +535,8 @@@
               * Add a filter for parks in France.
               */
              final FeatureQuery query = new FeatureQuery();
 -            query.setSortBy(FF.sort(FF.property("native_name"), 
SortOrder.DESCENDING));
              query.setSelection(FF.equal(FF.property("country"), 
FF.literal("FRA")));
-             query.setProjection(new 
FeatureQuery.NamedExpression(FF.property("native_name")));
+             query.setProjection(FF.property("native_name"));
              final FeatureSet frenchParks = parks.subset(query);
              /*
               * Verify the feature type.
diff --cc 
storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
index 11d8896,799c346..f9a9160
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
@@@ -35,19 -37,22 +37,20 @@@ import org.apache.sis.internal.storage.
  import org.apache.sis.filter.DefaultFilterFactory;
  import org.apache.sis.filter.Optimization;
  import org.apache.sis.util.ArgumentChecks;
- import org.apache.sis.util.Classes;
+ import org.apache.sis.util.CharSequences;
  import org.apache.sis.util.collection.Containers;
  import org.apache.sis.util.iso.Names;
+ import org.apache.sis.util.resources.Vocabulary;
  
  // Branch-dependent imports
 -import org.opengis.feature.Feature;
 -import org.opengis.feature.FeatureType;
 -import org.opengis.filter.FilterFactory;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.ValueReference;
 -import org.opengis.filter.SortBy;
 -import org.opengis.filter.SortProperty;
 -import org.opengis.filter.InvalidFilterValueException;
 +import org.apache.sis.feature.AbstractFeature;
 +import org.apache.sis.feature.DefaultFeatureType;
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.internal.geoapi.filter.Literal;
 +import org.apache.sis.internal.geoapi.filter.ValueReference;
 +import org.apache.sis.internal.geoapi.filter.SortBy;
 +import org.apache.sis.internal.geoapi.filter.SortProperty;
  
  
  /**
@@@ -581,9 -586,65 +590,65 @@@ public class FeatureQuery extends Quer
          if (projection == null) {
              return valueType;           // All columns included: result is of 
the same type.
          }
+         int unnamedNumber = 0;          // Sequential number for unnamed 
expressions.
+         Set<String> names = null;       // Names already used, for avoiding 
collisions.
          final FeatureTypeBuilder ftb = new 
FeatureTypeBuilder().setName(valueType.getName());
-         for (int i=0; i<projection.length; i++) {
-             projection[i].expectedType(i, valueType, ftb);
+         for (int column = 0; column < projection.length; column++) {
+             /*
+              * For each property, get the expected type (mandatory) and its 
name (optional).
+              * A default name will be computed if no alias were explicitly 
given by user.
+              */
+             GenericName name = projection[column].alias;
+             final Expression<?,?> expression = projection[column].expression;
+             final FeatureExpression<?,?> fex = 
FeatureExpression.castOrCopy(expression);
+             final PropertyTypeBuilder resultType;
+             if (fex == null || (resultType = fex.expectedType(valueType, 
ftb)) == null) {
 -                throw new 
InvalidFilterValueException(Resources.format(Resources.Keys.InvalidExpression_2,
++                throw new 
IllegalArgumentException(Resources.format(Resources.Keys.InvalidExpression_2,
+                             
expression.getFunctionName().toInternationalString(), column));
+             }
+             if (name == null) {
+                 /*
+                  * Build a list of aliases declared by the user, for making 
sure that we do not collide with them.
+                  * No check for `GenericName` collision here because it was 
already verified by `setProjection(…)`.
+                  * We may have collision of their `String` representations 
however, which is okay.
+                  */
+                 if (names == null) {
+                     names = new 
HashSet<>(Containers.hashMapCapacity(projection.length));
+                     for (final NamedExpression p : projection) {
+                         if (p.alias != null) {
+                             names.add(p.alias.toString());
+                         }
+                     }
+                 }
+                 /*
+                  * If the expression is a `ValueReference`, the 
`PropertyType` instance can be taken directly
+                  * from the source feature (the Apache SIS implementation 
does just that). If the name is set,
+                  * then we assume that it is correct. Otherwise we take the 
tip of the XPath.
+                  */
+                 CharSequence text = null;
+                 if (expression instanceof ValueReference<?,?>) {
+                     final GenericName current = resultType.getName();
+                     if (current != null && names.add(current.toString())) {
+                         continue;
+                     }
+                     String xpath = ((ValueReference<?,?>) 
expression).getXPath().trim();
+                     xpath = xpath.substring(xpath.lastIndexOf('/') + 1);    
// Works also if '/' is not found.
+                     if (!(xpath.isEmpty() || names.contains(xpath))) {
+                         text = xpath;
+                     }
+                 }
+                 /*
+                  * If we still have no name at this point, create a name like 
"Unnamed #1".
+                  * Note that despite the use of `Vocabulary` resources, the 
name will be unlocalized
+                  * (for easier programmatic use) because `GenericName` 
implementation is designed for
+                  * providing localized names only if explicitly requested.
+                  */
+                 if (text == null) do {
+                     text = 
Vocabulary.formatInternational(Vocabulary.Keys.Unnamed_1, ++unnamedNumber);
+                 } while (!names.add(text.toString()));
+                 name = Names.createLocalName(null, null, text);
+             }
+             resultType.setName(name);
          }
          return ftb.build();
      }
diff --cc 
storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
index b3af6a8,a4d157d..0f79898
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
@@@ -60,33 -68,61 +63,61 @@@ public final strictfp class FeatureQuer
      private final FeatureQuery query;
  
      /**
-      * Creates a new test.
+      * Creates a new test with a feature type composed of two attributes and 
one association.
       */
      public FeatureQueryTest() {
-         final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
-         ftb.setName("Test");
+         FeatureTypeBuilder ftb;
+ 
+         // A dependency of the test feature type.
+         ftb = new FeatureTypeBuilder().setName("Dependency");
+         ftb.addAttribute(Integer.class).setName("value3");
 -        final FeatureType dependency = ftb.build();
++        final DefaultFeatureType dependency = ftb.build();
+ 
+         // Test feature type with attributes and association.
+         ftb = new FeatureTypeBuilder().setName("Test");
          ftb.addAttribute(Integer.class).setName("value1");
          ftb.addAttribute(Integer.class).setName("value2");
+         ftb.addAssociation(dependency).setName("dependency");
 -        final FeatureType type = ftb.build();
 -        features = new Feature[] {
 +        final DefaultFeatureType type = ftb.build();
 +        features = new AbstractFeature[] {
-             feature(type, 3, 1),
-             feature(type, 2, 2),
-             feature(type, 2, 1),
-             feature(type, 1, 1),
-             feature(type, 4, 1)
+             feature(type, null,       3, 1,  0),
+             feature(type, null,       2, 2,  0),
+             feature(type, dependency, 2, 1, 25),
+             feature(type, dependency, 1, 1, 18),
+             feature(type, null,       4, 1,  0)
          };
          featureSet = new MemoryFeatureSet(null, type, 
Arrays.asList(features));
          query      = new FeatureQuery();
      }
  
-     private static AbstractFeature feature(final DefaultFeatureType type, 
final int value1, final int value2) {
+     /**
+      * Creates an instance of the test feature type with the given values.
+      * The {@code value3} is stored only if {@code dependency} is non-null.
+      */
 -    private static Feature feature(final FeatureType type, final FeatureType 
dependency,
++    private static AbstractFeature feature(final DefaultFeatureType type, 
final DefaultFeatureType dependency,
+                                    final int value1, final int value2, final 
int value3)
+     {
 -        final Feature f = type.newInstance();
 +        final AbstractFeature f = type.newInstance();
          f.setPropertyValue("value1", value1);
          f.setPropertyValue("value2", value2);
+         if (dependency != null) {
 -            final Feature d = dependency.newInstance();
++            final AbstractFeature d = dependency.newInstance();
+             d.setPropertyValue("value3", value3);
+             f.setPropertyValue("dependency", d);
+         }
          return f;
      }
  
      /**
+      * Configures the query for returning a single instance and returns that 
instance.
+      */
 -    private Feature executeAndGetFirst() throws DataStoreException {
++    private AbstractFeature executeAndGetFirst() throws DataStoreException {
+         query.setLimit(1);
+         final FeatureSet subset = query.execute(featureSet);
+         return 
TestUtilities.getSingleton(subset.features(false).collect(Collectors.toList()));
+     }
+ 
+     /**
       * Executes the query and verify that the result is equal to the features 
at the given indices.
       *
       * @param  indices  indices of expected features.
@@@ -130,39 -166,75 +161,62 @@@
      }
  
      /**
 -     * Verifies the effect of {@link FeatureQuery#setSortBy(SortProperty[])}.
 -     *
 -     * @throws DataStoreException if an error occurred while executing the 
query.
 -     */
 -    @Test
 -    public void testSortBy() throws DataStoreException {
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
 -        query.setSortBy(ff.sort(ff.property("value1", Integer.class), 
SortOrder.ASCENDING),
 -                        ff.sort(ff.property("value2", Integer.class), 
SortOrder.DESCENDING));
 -        verifyQueryResult(3, 1, 2, 0, 4);
 -    }
 -
 -    /**
+      * Verifies the effect of {@link FeatureQuery#setSelection(Filter)}.
+      *
+      * @throws DataStoreException if an error occurred while executing the 
query.
+      */
+     @Test
+     public void testSelection() throws DataStoreException {
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
++        final DefaultFilterFactory<AbstractFeature,?,?> ff = 
DefaultFilterFactory.forFeatures();
+         query.setSelection(ff.equal(ff.property("value1", Integer.class),
 -                                    ff.literal(2), true, MatchAction.ALL));
++                                    ff.literal(2)));
+         verifyQueryResult(1, 2);
+     }
+ 
+     /**
+      * Tests {@link FeatureQuery#setSelection(Filter)} on complex features
+      * with a filter that follows associations.
+      *
+      * @throws DataStoreException if an error occurred while executing the 
query.
+      */
+     @Test
+     public void testSelectionThroughAssociation() throws DataStoreException {
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
++        final DefaultFilterFactory<AbstractFeature,?,?> ff = 
DefaultFilterFactory.forFeatures();
+         query.setSelection(ff.equal(ff.property("dependency/value3"), 
ff.literal(18)));
+         verifyQueryResult(3);
+     }
+ 
+     /**
       * Verifies the effect of {@link 
FeatureQuery#setProjection(FeatureQuery.Column[])}.
       *
       * @throws DataStoreException if an error occurred while executing the 
query.
       */
      @Test
      public void testProjection() throws DataStoreException {
-         final DefaultFilterFactory<AbstractFeature,?,?> factory = 
DefaultFilterFactory.forFeatures();
-         query.setProjection(new 
FeatureQuery.NamedExpression(factory.property("value1", Integer.class), 
(String) null),
-                             new 
FeatureQuery.NamedExpression(factory.property("value1", Integer.class), 
"renamed1"),
-                             new 
FeatureQuery.NamedExpression(factory.literal("a literal"), "computed"));
-         query.setLimit(1);
- 
-         final FeatureSet fs = query.execute(featureSet);
-         final AbstractFeature result = 
TestUtilities.getSingleton(fs.features(false).collect(Collectors.toList()));
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
++        final DefaultFilterFactory<AbstractFeature,?,?> ff = 
DefaultFilterFactory.forFeatures();
+         query.setProjection(new 
FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) 
null),
+                             new 
FeatureQuery.NamedExpression(ff.property("value1", Integer.class), "renamed1"),
+                             new FeatureQuery.NamedExpression(ff.literal("a 
literal"), "computed"));
  
          // Check result type.
-         final DefaultFeatureType resultType = result.getType();
 -        final Feature instance = executeAndGetFirst();
 -        final FeatureType resultType = instance.getType();
++        final AbstractFeature instance = executeAndGetFirst();
++        final DefaultFeatureType resultType = instance.getType();
          assertEquals("Test", resultType.getName().toString());
          assertEquals(3, resultType.getProperties(true).size());
 -        final PropertyType pt1 = resultType.getProperty("value1");
 -        final PropertyType pt2 = resultType.getProperty("renamed1");
 -        final PropertyType pt3 = resultType.getProperty("computed");
 -        assertTrue(pt1 instanceof AttributeType);
 -        assertTrue(pt2 instanceof AttributeType);
 -        assertTrue(pt3 instanceof AttributeType);
 -        assertEquals(Integer.class, ((AttributeType) pt1).getValueClass());
 -        assertEquals(Integer.class, ((AttributeType) pt2).getValueClass());
 -        assertEquals(String.class,  ((AttributeType) pt3).getValueClass());
 +        final AbstractIdentifiedType pt1 = resultType.getProperty("value1");
 +        final AbstractIdentifiedType pt2 = resultType.getProperty("renamed1");
 +        final AbstractIdentifiedType pt3 = resultType.getProperty("computed");
 +        assertTrue(pt1 instanceof DefaultAttributeType);
 +        assertTrue(pt2 instanceof DefaultAttributeType);
 +        assertTrue(pt3 instanceof DefaultAttributeType);
 +        assertEquals(Integer.class, ((DefaultAttributeType) 
pt1).getValueClass());
 +        assertEquals(Integer.class, ((DefaultAttributeType) 
pt2).getValueClass());
 +        assertEquals(String.class,  ((DefaultAttributeType) 
pt3).getValueClass());
  
-         // Check feature.
-         assertEquals(3, result.getPropertyValue("value1"));
-         assertEquals(3, result.getPropertyValue("renamed1"));
-         assertEquals("a literal", result.getPropertyValue("computed"));
+         // Check feature instance.
+         assertEquals(3, instance.getPropertyValue("value1"));
+         assertEquals(3, instance.getPropertyValue("renamed1"));
+         assertEquals("a literal", instance.getPropertyValue("computed"));
      }
  
      /**
@@@ -173,10 -245,79 +227,79 @@@
      @Test
      public void testProjectionByNames() throws DataStoreException {
          query.setProjection("value2");
-         query.setLimit(1);
-         final FeatureSet  fs = query.execute(featureSet);
-         final AbstractFeature result = 
TestUtilities.getSingleton(fs.features(false).collect(Collectors.toList()));
-         final AbstractIdentifiedType p = 
TestUtilities.getSingleton(result.getType().getProperties(true));
 -        final Feature instance = executeAndGetFirst();
 -        final PropertyType p = 
TestUtilities.getSingleton(instance.getType().getProperties(true));
++        final AbstractFeature instance = executeAndGetFirst();
++        final AbstractIdentifiedType p = 
TestUtilities.getSingleton(instance.getType().getProperties(true));
          assertEquals("value2", p.getName().toString());
      }
+ 
+     /**
+      * Tests the creation of default column names when no alias where 
explicitly specified.
+      * Note that the string representations of default names shall be 
unlocalized.
+      *
+      * @throws DataStoreException if an error occurred while executing the 
query.
+      */
+     @Test
+     public void testDefaultColumnName() throws DataStoreException {
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
++        final DefaultFilterFactory<AbstractFeature,?,?> ff = 
DefaultFilterFactory.forFeatures();
+         query.setLimit(1);
+         query.setProjection(
+                 ff.add(ff.property("value1", Number.class), ff.literal(1)),
+                 ff.add(ff.property("value2", Number.class), ff.literal(1)));
+         final FeatureSet subset = featureSet.subset(query);
 -        final FeatureType type = subset.getType();
 -        final Iterator<? extends PropertyType> properties = 
type.getProperties(true).iterator();
++        final DefaultFeatureType type = subset.getType();
++        final Iterator<? extends AbstractIdentifiedType> properties = 
type.getProperties(true).iterator();
+         assertEquals("Unnamed #1", properties.next().getName().toString());
+         assertEquals("Unnamed #2", properties.next().getName().toString());
+         assertFalse(properties.hasNext());
+ 
 -        final Feature instance = 
TestUtilities.getSingleton(subset.features(false).collect(Collectors.toList()));
++        final AbstractFeature instance = 
TestUtilities.getSingleton(subset.features(false).collect(Collectors.toList()));
+         assertSame(type, instance.getType());
+     }
+ 
+     /**
+      * Tests {@link 
FeatureQuery#setProjection(FeatureQuery.NamedExpression...)} on an abstract 
feature type.
+      * We expect the column to be defined even if the property name is 
undefined on the feature type.
+      * This case happens when the {@link FeatureSet} contains features with 
inherited types.
+      *
+      * @throws DataStoreException if an error occurred while executing the 
query.
+      */
+     @Test
+     public void testProjectionOfAbstractType() throws DataStoreException {
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
++        final DefaultFilterFactory<AbstractFeature,?,?> ff = 
DefaultFilterFactory.forFeatures();
+         query.setProjection(new 
FeatureQuery.NamedExpression(ff.property("value1"),  (String) null),
+                             new 
FeatureQuery.NamedExpression(ff.property("/*/unknown"), "unexpected"));
+ 
+         // Check result type.
 -        final Feature instance = executeAndGetFirst();
 -        final FeatureType resultType = instance.getType();
++        final AbstractFeature instance = executeAndGetFirst();
++        final DefaultFeatureType resultType = instance.getType();
+         assertEquals("Test", resultType.getName().toString());
+         assertEquals(2, resultType.getProperties(true).size());
 -        final PropertyType pt1 = resultType.getProperty("value1");
 -        final PropertyType pt2 = resultType.getProperty("unexpected");
 -        assertTrue(pt1 instanceof AttributeType);
 -        assertTrue(pt2 instanceof AttributeType);
 -        assertEquals(Integer.class, ((AttributeType) pt1).getValueClass());
 -        assertEquals(Object.class,  ((AttributeType) pt2).getValueClass());
++        final AbstractIdentifiedType pt1 = resultType.getProperty("value1");
++        final AbstractIdentifiedType pt2 = 
resultType.getProperty("unexpected");
++        assertTrue(pt1 instanceof DefaultAttributeType);
++        assertTrue(pt2 instanceof DefaultAttributeType);
++        assertEquals(Integer.class, ((DefaultAttributeType) 
pt1).getValueClass());
++        assertEquals(Object.class,  ((DefaultAttributeType) 
pt2).getValueClass());
+ 
+         // Check feature property values.
+         assertEquals(3,    instance.getPropertyValue("value1"));
+         assertEquals(null, instance.getPropertyValue("unexpected"));
+     }
+ 
+     /**
+      * Tests {@link 
FeatureQuery#setProjection(FeatureQuery.NamedExpression...)} on complex features
+      * with a filter that follows associations.
+      *
+      * @throws DataStoreException if an error occurred while executing the 
query.
+      */
+     @Test
+     public void testProjectionThroughAssociation() throws DataStoreException {
 -        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
++        final DefaultFilterFactory<AbstractFeature,?,?> ff = 
DefaultFilterFactory.forFeatures();
+         query.setProjection(new 
FeatureQuery.NamedExpression(ff.property("value1"),  (String) null),
+                             new 
FeatureQuery.NamedExpression(ff.property("dependency/value3"), "value3"));
+         query.setOffset(2);
 -        final Feature instance = executeAndGetFirst();
++        final AbstractFeature instance = executeAndGetFirst();
+         assertEquals("value1",  2, instance.getPropertyValue("value1"));
+         assertEquals("value3", 25, instance.getPropertyValue("value3"));
+     }
  }

Reply via email to