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

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

commit 9c433dee6c7c5a9d3b3293d4053e1cff8da8e098
Merge: b0c524e a34b9e8
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Sat Jan 1 23:19:44 2022 +0100

    Merge branch 'feat/featureset-subset-abstract' into geoapi-4.0 but with
    a support of simple x-paths instead of unconditional catch of exception.
    The exception is caught only if the xpath is something like "/*/property"
    where the root "/" in interpreted as the feature instance and the "*" is
    interpreted as "any feature, including types not known to the planner".

 .../org/apache/sis/filter/AssociationValue.java    | 234 +++++++++++++++++++++
 .../apache/sis/filter/DefaultFilterFactory.java    |   4 +-
 .../java/org/apache/sis/filter/PropertyValue.java  | 205 +++++++++++-------
 .../org/apache/sis/filter/LogicalFilterTest.java   |   4 +-
 .../java/org/apache/sis/internal/util/XPaths.java  |  51 ++++-
 .../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/internal/util/XPathsTest.java   |  15 +-
 .../java/org/apache/sis/storage/FeatureQuery.java  |   6 +-
 .../org/apache/sis/storage/FeatureQueryTest.java   | 151 ++++++++++---
 11 files changed, 561 insertions(+), 116 deletions(-)

diff --cc 
core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java
index 0000000,0000000..a46b3bb
new file mode 100644
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java
@@@ -1,0 -1,0 +1,234 @@@
++/*
++ * 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;
++
++
++/**
++ * 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>
++{
++    /**
++     * 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)}.
++     * 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;
++    }
++
++    /**
++     * 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) {
++walk:   if (instance != null) {
++            for (final String p : path) {
++                final Object value = instance.getPropertyValue(p);
++                if (!(value instanceof Feature)) break walk;
++                instance = (Feature) 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();
++walk:   if (specifiedType != null) try {
++            FeatureType 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]);
++                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();
++            }
++            /*
++             * 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) {
++            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) 
{
++        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) {
++        for (final String p : path) {
++            final PropertyType type;
++            try {
++                type = valueType.getProperty(p);
++            } catch (PropertyNotFoundException 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)) {
++                return null;
++            }
++            valueType = ((FeatureAssociationRole) 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/DefaultFilterFactory.java
index b9b495a,b9b495a..158c2f9
--- 
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
@@@ -48,7 -48,7 +48,7 @@@ import org.opengis.filter.capability.Fi
   *
   * @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  <G>  base class of geometry objects. The implementation-neutral 
type is GeoAPI {@link Geometry},
@@@ -227,6 -227,6 +227,8 @@@ public abstract class DefaultFilterFact
           */
          @Override
          public <V> ValueReference<Feature,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 89714ff,1638c07..0f19225
--- 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
@@@ -16,17 -16,17 +16,20 @@@
   */
  package org.apache.sis.filter;
  
++import java.util.Arrays;
++import java.util.List;
  import java.util.Optional;
  import java.util.Collection;
  import java.util.Collections;
  import org.apache.sis.feature.Features;
--import org.apache.sis.util.ArgumentChecks;
  import org.apache.sis.util.ObjectConverter;
  import org.apache.sis.util.ObjectConverters;
  import org.apache.sis.util.UnconvertibleObjectException;
  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;
@@@ -36,7 -36,7 +39,6 @@@ import org.opengis.feature.AttributeTyp
  import org.opengis.feature.IdentifiedType;
  import org.opengis.feature.Operation;
  import org.opengis.feature.PropertyNotFoundException;
--import org.opengis.filter.Expression;
  import org.opengis.filter.ValueReference;
  
  
@@@ -51,10 -51,10 +53,14 @@@
   *
   * @param  <V>  the type of value computed by the expression.
   *
++ * @see AssociationValue
++ *
   * @since 1.1
   * @module
   */
--abstract class PropertyValue<V> extends LeafExpression<Feature,V> implements 
ValueReference<Feature,V> {
++abstract class PropertyValue<V> extends LeafExpression<Feature,V>
++        implements ValueReference<Feature,V>, 
Optimization.OnExpression<Feature,V>
++{
      /**
       * For cross-version compatibility.
       */
@@@ -62,33 -62,33 +68,73 @@@
  
      /**
       * Name of the property from which to retrieve the value.
++     * This is the argument to give in calls to {@link 
Feature#getProperty(String)}
       */
      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;
      }
  
      /**
--     * 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) {
++        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 is 
guaranteed non-empty.
++            isVirtual = head.equals("/*");
++            if (isVirtual || head.charAt(0) != '/') {       // Components are 
guaranteed non-empty.
++                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);
      }
  
      /**
@@@ -96,7 -96,7 +142,7 @@@
       */
      @Override
      protected final Collection<?> getChildren() {
--        return Collections.singleton(name);
++        return isVirtual ? Arrays.asList(name, isVirtual) : 
Collections.singleton(name);
      }
  
      /**
@@@ -104,33 -104,33 +150,53 @@@
       */
      @Override
      public final String getXPath() {
--        return name;
++        return isVirtual ? VIRTUAL_PREFIX.concat(name) : name;
      }
  
      /**
       * Returns the type of values fetched from {@link Feature} 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;
      }
  
      /**
++     * Returns the default value of {@link #expectedType(FeatureType, 
FeatureTypeBuilder)}
++     * when it can not be inferred by the analysis of the given {@code 
FeatureType}.
++     */
++    final PropertyTypeBuilder expectedType(final FeatureTypeBuilder addTo) {
++        return 
addTo.addAttribute(getValueClass()).setName(name).setMinimumOccurs(0);
++    }
++
++    /**
       * 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> type) {
--        if (type.isAssignableFrom(getValueClass())) {
++    public final <N> PropertyValue<N> toValueType(final Class<N> target) {
++        if (target.equals(getValueClass())) {
              return (PropertyValue<N>) this;
          }
          final Class<?> source = getSourceClass();
--        if (source != Object.class) {
--            return new CastedAndConverted<>(source, type, name);
++        if (target == Object.class) {
++            return (PropertyValue<N>) new AsObject(name, isVirtual);
++        } else if (source == Object.class) {
++            return new Converted<>(target, name, isVirtual);
++        } else {
++            return new CastedAndConverted<>(source, target, name, isVirtual);
          }
--        return create(name, type);
      }
  
++    /**
++     * If the evaluated property is a link, replaces this expression by a 
more direct reference
++     * to the target property. This optimization is important for allowing 
{@code SQLStore} to
++     * put the column name in the SQL {@code WHERE} clause. It makes the 
difference between
++     * using or not the database index.
++     */
++    public abstract PropertyValue<V> optimize(Optimization optimization);
  
  
  
@@@ -138,21 -138,21 +204,19 @@@
       * An expression fetching property values as {@code Object}.
       * This expression does not need to apply any type conversion.
       */
--    private static final class AsObject extends PropertyValue<Object>
--            implements Optimization.OnExpression<Feature,Object>
--    {
++    private static final class AsObject extends PropertyValue<Object> {
          /** For cross-version compatibility. */
          private static final long serialVersionUID = 2854731969723006038L;
  
          /**
           * Creates a new expression retrieving values from a property of the 
given name.
           */
--        AsObject(final String name) {
--            super(name);
++        AsObject(final String name, final boolean isVirtual) {
++            super(name, isVirtual);
          }
  
          /**
--         * Returns the value of the property of the given name.
++         * Returns the value of the property of the name given at 
construction time.
           * If no value is found for the given feature, then this method 
returns {@code null}.
           */
          @Override
@@@ -173,10 -173,10 +237,11 @@@
           * using or not the database index.
           */
          @Override
--        public Expression<Feature,?> optimize(final Optimization 
optimization) {
++        public PropertyValue<Object> optimize(final Optimization 
optimization) {
              final FeatureType 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) {
                  warning(e, true);
              }
@@@ -191,7 -191,7 +256,7 @@@
       * An expression fetching property values as an object of specified type.
       * The value is converted from {@link Object} to the specified type.
       */
--    private static class Converted<V> extends PropertyValue<V> implements 
Optimization.OnExpression<Feature,V> {
++    private static class Converted<V> extends PropertyValue<V> {
          /** For cross-version compatibility. */
          private static final long serialVersionUID = -1436865010478207066L;
  
@@@ -204,20 -204,20 +269,12 @@@
           * @param  type  the desired type for the expression result.
           * @param  name  the name of the property to fetch.
           */
--        protected Converted(final Class<V> type, final String name) {
--            super(name);
++        protected Converted(final Class<V> type, final String xpath, final 
boolean isVirtual) {
++            super(xpath, isVirtual);
              this.type = type;
          }
  
          /**
--         * Creates a new {@code Converted} fetching values for a property of 
different name.
--         * The given name should be the target of a link that the caller has 
resolved.
--         */
--        protected Converted<V> rename(final String target) {
--            return new Converted<>(type, target);
--        }
--
--        /**
           * Returns the type of values computed by this expression.
           */
          @Override
@@@ -248,27 -248,27 +305,40 @@@
           * then a specialized expression is returned. Otherwise this method 
returns {@code this}.
           */
          @Override
--        public final Expression<Feature, ? extends V> optimize(final 
Optimization optimization) {
++        public final PropertyValue<V> optimize(final Optimization 
optimization) {
              final FeatureType featureType = optimization.getFeatureType();
              if (featureType != null) try {
--                String targetName = name;
--                PropertyType 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);
                  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) {
--                    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<?>) {
--                    final Class<?> source = ((AttributeType<?>) 
property).getValueClass();
--                    if (source != null && source != Object.class && 
!source.isAssignableFrom(getSourceClass())) {
--                        return new CastedAndConverted<>(source, type, 
targetName);
--                    }
++                    source = ((AttributeType<?>) 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) {
                  warning(e, true);
@@@ -303,7 -303,13 +373,16 @@@
       */
      @Override
      public PropertyTypeBuilder expectedType(final FeatureType valueType, 
final FeatureTypeBuilder addTo) {
-         PropertyType type = valueType.getProperty(name);        // May throw 
IllegalArgumentException.
+         PropertyType type;
+         try {
+             type = valueType.getProperty(name);
 -        } catch (IllegalArgumentException ex) {
 -            // the property does not exist but may be defined on a yet 
unknown child type.
 -            return 
addTo.addAttribute(Object.class).setName(name).setMinimumOccurs(0);
++        } catch (PropertyNotFoundException 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) {
@@@ -336,28 -342,28 +415,12 @@@
          private final ObjectConverter<? super S, ? extends V> converter;
  
          /** Creates a new expression retrieving values from a property of the 
given name. */
--        CastedAndConverted(final Class<S> source, final Class<V> type, final 
String name) {
--            super(type, name);
++        CastedAndConverted(final Class<S> source, final Class<V> type, final 
String xpath, final boolean isVirtual) {
++            super(type, xpath, isVirtual);
              this.source = source;
              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.
           */
diff --cc 
core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFilterTest.java
index a600504,a600504..8dd829b
--- 
a/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFilterTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFilterTest.java
@@@ -229,7 -229,7 +229,7 @@@ public final strictfp class LogicalFilt
          assertNotSame(e, opt);
  
          final PropertyValue<?> p = (PropertyValue<?>) 
opt.getParameters().get(0);
--        assertEquals(String.class,  p.getSourceClass());
--        assertEquals(Integer.class, p.getValueClass());
++        assertEquals(String.class, p.getSourceClass());
++        assertEquals(Number.class, p.getValueClass());
      }
  }
diff --cc 
core/sis-utility/src/main/java/org/apache/sis/internal/util/XPaths.java
index 9e528b4,9e528b4..d432f57
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPaths.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPaths.java
@@@ -16,17 -16,17 +16,23 @@@
   */
  package org.apache.sis.internal.util;
  
++import java.util.List;
++import java.util.ArrayList;
  import org.apache.sis.util.Static;
  
  import static org.apache.sis.util.CharSequences.*;
  import static org.apache.sis.internal.util.DefinitionURI.regionMatches;
++import org.apache.sis.util.resources.Errors;
  
  
  /**
-- * Utility methods related to x-paths.
++ * Utility methods related to x-paths. This is intended to be only a 
lightweight support;
++ * this is not a replacement for {@link javax.xml.xpath} implementations. 
This is used as
++ * a place where to centralize XPath handling for possible replacement by a 
more advanced
++ * framework in the future.
   *
   * @author  Martin Desruisseaux (Geomatys)
-- * @version 0.8
++ * @version 1.2
   * @since   0.4
   * @module
   */
@@@ -50,8 -50,8 +56,6 @@@ public final class XPaths extends Stati
       * @param  offset  index of the first character to verify.
       * @return index after the last character of the presumed URI, or -1 if 
this
       *         method thinks that the given character sequence is not a URI.
--     *
--     * @since 0.8
       */
      public static int endOfURI(final CharSequence uri, int offset) {
          boolean isURI = false;
@@@ -83,6 -83,6 +87,45 @@@ scan:   while (offset < length) 
      }
  
      /**
++     * Splits the given URL around the {@code '/'} separator, or returns 
{@code null} if there is no separator.
++     * By convention if the URL is absolute, then the leading {@code '/'} 
character is kept in the first element.
++     * For example {@code "/∗/property"} is splitted as two elements: {@code 
"/∗"} and {@code "property"}.
++     *
++     * <p>This method trims the whitespaces of components except the last one 
(the tip),
++     * for consistency with the case where this method returns {@code 
null}.</p>
++     *
++     * @param  xpath  the URL to split.
++     * @return the splitted URL with the heading separator kept in the first 
element, or {@code null}
++     *         if there is no separator. If non-null, the list always 
contains at least one element.
++     * @throws IllegalArgumentException if the XPath contains at least one 
empty component.
++     */
++    public static List<String> split(final String xpath) {
++        int next = xpath.indexOf('/');
++        if (next < 0) {
++            return null;
++        }
++        final List<String> components = new ArrayList<>(4);
++        int start = skipLeadingWhitespaces(xpath, 0, next);
++        if (start < next) {
++            // No leading '/' (the characters before it are a path element, 
added below).
++            components.add(xpath.substring(start, 
skipTrailingWhitespaces(xpath, start, next)));
++            start = ++next;
++        } else {
++            // Keep the `start` position on the leading '/'.
++            next++;
++        }
++        while ((next = xpath.indexOf('/', next)) >= 0) {
++            components.add(trimWhitespaces(xpath, start, next).toString());
++            start = ++next;
++        }
++        components.add(xpath.substring(start));         // No whitespace 
trimming.
++        if (components.stream().anyMatch(String::isEmpty)) {
++            throw new 
IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedXPath_1, xpath));
++        }
++        return components;
++    }
++
++    /**
       * Parses a URL which contains a pointer to a XML fragment.
       * The current implementation recognizes the following types:
       *
diff --cc 
core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
index 474444d,474444d..030b59e
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
@@@ -1028,6 -1028,6 +1028,11 @@@ public final class Errors extends Index
          public static final short UnsupportedType_1 = 163;
  
          /**
++         * XPath “{0}” is not supported.
++         */
++        public static final short UnsupportedXPath_1 = 195;
++
++        /**
           * A value is already defined for “{0}”.
           */
          public static final short ValueAlreadyDefined_1 = 164;
diff --cc 
core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
index 684af4c,684af4c..5b2d938
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
@@@ -216,6 -216,6 +216,7 @@@ UnsupportedAxisDirection_1        = Axe
  UnsupportedCoordinateSystem_1     = The \u201c{0}\u201d coordinate system is 
not supported by this operation.
  UnsupportedDatum_1                = The \u201c{0}\u201d datum is not 
supported by this operation.
  UnsupportedType_1                 = The \u2018{0}\u2019 type is not supported 
in this context.
++UnsupportedXPath_1                = XPath \u201c{0}\u201d is not supported.
  ValueAlreadyDefined_1             = A value is already defined for 
\u201c{0}\u201d.
  ValueNotGreaterThanZero_2         = Value \u2018{0}\u2019 = {1,number} is 
invalid. Expected a number greater than 0.
  ValueOutOfRange_4                 = Value \u2018{0}\u2019 = {3} is invalid. 
Expected a value in the [{1} \u2026 {2}] range.
diff --cc 
core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
index d4cd22d,d4cd22d..6e99108
--- 
a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
+++ 
b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
@@@ -212,6 -212,6 +212,7 @@@ UnsupportedAxisDirection_1        = Le
  UnsupportedCoordinateSystem_1     = Le syst\u00e8me de coordonn\u00e9es 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas support\u00e9 par cette 
op\u00e9ration.
  UnsupportedDatum_1                = Le r\u00e9f\u00e9rentiel 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas support\u00e9 par cette 
op\u00e9ration.
  UnsupportedType_1                 = Le type \u2018{0}\u2019 n\u2019est pas 
support\u00e9 dans ce contexte.
++UnsupportedXPath_1                = Le chemin x-path 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas support\u00e9.
  ValueAlreadyDefined_1             = Une valeur est d\u00e9j\u00e0 
d\u00e9finie pour \u00ab\u202f{0}\u202f\u00bb.
  ValueNotGreaterThanZero_2         = La valeur \u2018{0}\u2019 = {1,number} 
n\u2019est pas valide. On attendait un nombre positif non-nul.
  ValueOutOfRange_4                 = La valeur \u2018{0}\u2019 = {3} est 
invalide. Une valeur dans la plage [{1} \u2026 {2}] \u00e9tait attendue.
diff --cc 
core/sis-utility/src/test/java/org/apache/sis/internal/util/XPathsTest.java
index 4f29043,4f29043..46a08e1
--- 
a/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPathsTest.java
+++ 
b/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPathsTest.java
@@@ -27,15 -27,15 +27,13 @@@ import static org.junit.Assert.*
   * Tests {@link XPaths}.
   *
   * @author  Martin Desruisseaux (Geomatys)
-- * @version 1.1
++ * @version 1.2
   * @since   0.4
   * @module
   */
  public final strictfp class XPathsTest extends TestCase {
      /**
       * Tests the {@link XPaths#endOfURI(CharSequence, int)} method.
--     *
--     * @since 0.8
       */
      @Test
      public void testEndOfURI() {
@@@ -48,6 -48,6 +46,17 @@@
      }
  
      /**
++     * Tests {@link XPaths#split(String)}.
++     */
++    @Test
++    public void testSplit() {
++        assertNull(XPaths.split("property"));
++        assertArrayEquals(new String[] {"/property"},                    
XPaths.split("/property").toArray());
++        assertArrayEquals(new String[] {"Feature", "property", "child"}, 
XPaths.split("Feature/property/child").toArray());
++        assertArrayEquals(new String[] {"/Feature", "property"},         
XPaths.split("/Feature/property").toArray());
++    }
++
++    /**
       * Tests {@link XPaths#xpointer(String, String)}.
       */
      @Test
diff --cc 
storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
index 6b5626d,6b5626d..799c346
--- 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
@@@ -674,7 -674,7 +674,7 @@@ public class FeatureQuery extends Quer
       */
      @Override
      public int hashCode() {
--        return 97 * Arrays.hashCode(projection) + 31 * selection.hashCode()
++        return 97 * Arrays.hashCode(projection) + 31 * 
Objects.hashCode(selection)
                + 7 * Objects.hashCode(sortBy) + Long.hashCode(limit ^ skip)
                + 3 * Objects.hashCode(linearResolution);
      }
@@@ -694,7 -694,7 +694,7 @@@
              final FeatureQuery other = (FeatureQuery) obj;
              return skip  == other.skip &&
                     limit == other.limit &&
--                   selection.equals(other.selection) &&
++                   Objects.equals(selection,        other.selection) &&
                     Arrays .equals(projection,       other.projection) &&
                     Objects.equals(sortBy,           other.sortBy) &&
                     Objects.equals(linearResolution, other.linearResolution);
@@@ -720,7 -720,7 +720,7 @@@
          } else {
              sb.append('*');
          }
--        if (selection != Filter.include()) {
++        if (selection != null) {
              sb.append(" WHERE ").append(selection);
          }
          if (sortBy != null) {
diff --cc 
storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
index 7f6af73,3c0516c..a4d157d
--- 
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
@@@ -68,33 -68,33 +68,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();
++
++        // 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[] {
--            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 Feature feature(final FeatureType 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,
++                                   final int value1, final int value2, final 
int value3)
++    {
          final Feature f = type.newInstance();
          f.setPropertyValue("value1", value1);
          f.setPropertyValue("value2", value2);
++        if (dependency != null) {
++            final Feature 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 {
++        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.
@@@ -144,9 -144,9 +172,9 @@@
       */
      @Test
      public void testSortBy() throws DataStoreException {
--        final FilterFactory<Feature,?,?> factory = 
DefaultFilterFactory.forFeatures();
--        query.setSortBy(factory.sort(factory.property("value1", 
Integer.class), SortOrder.ASCENDING),
--                        factory.sort(factory.property("value2", 
Integer.class), SortOrder.DESCENDING));
++        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);
      }
  
@@@ -157,30 -157,30 +185,40 @@@
       */
      @Test
      public void testSelection() throws DataStoreException {
--        final FilterFactory<Feature,?,?> factory = 
DefaultFilterFactory.forFeatures();
--        query.setSelection(factory.equal(factory.property("value1", 
Integer.class),
--                                      factory.literal(2), true, 
MatchAction.ALL));
++        final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
++        query.setSelection(ff.equal(ff.property("value1", Integer.class),
++                                    ff.literal(2), true, MatchAction.ALL));
          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();
++        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 FilterFactory<Feature,?,?> 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 Feature result = 
TestUtilities.getSingleton(fs.features(false).collect(Collectors.toList()));
++        final FilterFactory<Feature,?,?> 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 FeatureType resultType = result.getType();
++        final Feature instance = executeAndGetFirst();
++        final FeatureType resultType = instance.getType();
          assertEquals("Test", resultType.getName().toString());
          assertEquals(3, resultType.getProperties(true).size());
          final PropertyType pt1 = resultType.getProperty("value1");
@@@ -193,10 -193,10 +231,10 @@@
          assertEquals(Integer.class, ((AttributeType) pt2).getValueClass());
          assertEquals(String.class,  ((AttributeType) 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"));
      }
  
      /**
@@@ -207,10 -207,10 +245,8 @@@
      @Test
      public void testProjectionByNames() throws DataStoreException {
          query.setProjection("value2");
--        query.setLimit(1);
--        final FeatureSet  fs = query.execute(featureSet);
--        final Feature result = 
TestUtilities.getSingleton(fs.features(false).collect(Collectors.toList()));
--        final PropertyType p = 
TestUtilities.getSingleton(result.getType().getProperties(true));
++        final Feature instance = executeAndGetFirst();
++        final PropertyType p = 
TestUtilities.getSingleton(instance.getType().getProperties(true));
          assertEquals("value2", p.getName().toString());
      }
  
@@@ -223,6 -223,6 +259,7 @@@
      @Test
      public void testDefaultColumnName() throws DataStoreException {
          final FilterFactory<Feature,?,?> 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)));
@@@ -232,5 -232,38 +269,55 @@@
          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()));
++        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 testColumnsAbstractType() throws DataStoreException {
++    public void testProjectionOfAbstractType() throws DataStoreException {
+         final FilterFactory<Feature,?,?> ff = 
DefaultFilterFactory.forFeatures();
+         query.setProjection(new 
FeatureQuery.NamedExpression(ff.property("value1"),  (String) null),
 -                            new 
FeatureQuery.NamedExpression(ff.property("unknown"), "unexpected"));
 -        query.setLimit(1);
 -
 -        final FeatureSet fs = query.execute(featureSet);
 -        final Feature result = 
TestUtilities.getSingleton(fs.features(false).collect(Collectors.toList()));
++                            new 
FeatureQuery.NamedExpression(ff.property("/*/unknown"), "unexpected"));
+ 
+         // Check result type.
 -        final FeatureType resultType = result.getType();
++        final Feature instance = executeAndGetFirst();
++        final FeatureType 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());
+ 
+         // Check feature property values.
 -        assertEquals(3,    result.getPropertyValue("value1"));
 -        assertEquals(null, result.getPropertyValue("unexpected"));
++        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();
++        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();
++        assertEquals("value1",  2, instance.getPropertyValue("value1"));
++        assertEquals("value3", 25, instance.getPropertyValue("value3"));
      }
  }

Reply via email to