You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/01/01 22:24:41 UTC
[sis] 01/01: 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".
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 <ma...@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"));
}
}