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")); } }