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 2025/05/13 17:37:09 UTC
(sis) 04/04: `SQLStore` should accept queries with a "projection" (in SQL sense) that contains links such as "sis:identifier".
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 0db3b21989774006e39c393c526524879efc2b7c
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue May 13 07:26:26 2025 +0200
`SQLStore` should accept queries with a "projection" (in SQL sense) that contains links such as "sis:identifier".
https://issues.apache.org/jira/browse/SIS-609
---
.../org.apache.sis.feature/main/module-info.java | 1 +
.../org/apache/sis/feature/AbstractOperation.java | 4 +-
.../apache/sis/feature/ExpressionOperation.java | 2 +-
.../org/apache/sis/feature/FeatureOperations.java | 48 +-
.../sis/feature/builder/OperationWrapper.java | 30 +-
.../main/org/apache/sis/feature/package-info.java | 2 +-
.../sis/feature/privy/FeatureExpression.java | 89 +--
.../sis/feature/privy/FeatureProjection.java | 341 +++++++++++
.../feature/privy/FeatureProjectionBuilder.java | 682 +++++++++++++++++++++
.../org/apache/sis/feature/privy/FeatureView.java | 131 ++++
.../org/apache/sis/filter/ArithmeticFunction.java | 8 +-
.../org/apache/sis/filter/AssociationValue.java | 22 +-
.../org/apache/sis/filter/ConvertFunction.java | 19 +-
.../apache/sis/filter/DefaultFilterFactory.java | 25 +
.../apache/sis/filter/InvalidXPathException.java | 81 +++
.../main/org/apache/sis/filter/LeafExpression.java | 15 +-
.../main/org/apache/sis/filter/PropertyValue.java | 75 ++-
.../main/org/apache/sis/filter/privy/XPath.java | 18 +-
.../apache/sis/filter/sqlmm/FunctionWithSRID.java | 24 +-
.../apache/sis/filter/sqlmm/SpatialFunction.java | 36 +-
.../sis/storage/sql/feature/FeatureIterator.java | 31 +-
.../sis/storage/sql/feature/FeatureStream.java | 38 +-
.../org/apache/sis/storage/sql/feature/Table.java | 46 +-
.../main/org/apache/sis/storage/FeatureQuery.java | 59 +-
.../main/org/apache/sis/storage/FeatureSubset.java | 6 +-
.../apache/sis/storage/base/FeatureProjection.java | 383 ------------
26 files changed, 1598 insertions(+), 618 deletions(-)
diff --git a/endorsed/src/org.apache.sis.feature/main/module-info.java b/endorsed/src/org.apache.sis.feature/main/module-info.java
index 86a5451ee6..9518b5edcc 100644
--- a/endorsed/src/org.apache.sis.feature/main/module-info.java
+++ b/endorsed/src/org.apache.sis.feature/main/module-info.java
@@ -51,6 +51,7 @@ module org.apache.sis.feature {
exports org.apache.sis.feature.privy to
org.apache.sis.storage,
org.apache.sis.storage.xml,
+ org.apache.sis.storage.sql,
org.apache.sis.storage.netcdf,
org.apache.sis.storage.shapefile, // In the "incubator" sub-project.
org.apache.sis.storage.gdal, // In the "optional" sub-project.
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
index a498244380..cd7d311557 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java
@@ -204,8 +204,8 @@ public abstract class AbstractOperation extends AbstractIdentifiedType implement
/**
* Returns the names of feature properties that this operation needs for performing its task.
* This method does not resolve transitive dependencies, i.e. if a dependency is itself an operation having
- * other dependencies, the returned set will contain the name of that operation but not the names of that
- * operation dependencies (unless they are the same that the direct dependencies of {@code this}).
+ * other dependencies, the returned set will contain the name of that operation but not the names of the
+ * dependencies of that operation (unless they are the same that the direct dependencies of {@code this}).
*
* <div class="note"><b>Rational:</b>
* this information is needed for writing the {@code SELECT} SQL statement to send to a database server.
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
index e01675cc6b..5d27304400 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
@@ -62,7 +62,7 @@ final class ExpressionOperation<V> extends AbstractOperation {
* The expression to which to delegate the execution of this operation.
*/
@SuppressWarnings("serial") // Not statically typed as serializable.
- private final Function<? super Feature, ? extends V> expression;
+ final Function<? super Feature, ? extends V> expression;
/**
* The type of result of evaluating the expression.
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
index f1ca57ee9a..05210b25e4 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
@@ -27,6 +27,8 @@ import org.apache.sis.util.Static;
import org.apache.sis.util.collection.WeakHashSet;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.privy.Strings;
+import org.apache.sis.filter.DefaultFilterFactory;
+import org.apache.sis.filter.privy.XPath;
import org.apache.sis.setup.GeometryLibrary;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -103,7 +105,7 @@ import org.opengis.filter.Expression;
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
* @since 0.7
*/
public final class FeatureOperations extends Static {
@@ -306,7 +308,7 @@ public final class FeatureOperations extends Static {
* @param <V> the type of values computed by the expression and assigned to the feature property.
* @param identification the name of the operation, together with optional information.
* @param expression the expression to evaluate on feature instances.
- * @param resultType type of values computed by the expression and assigned to the feature property.
+ * @param resultType type of values computed by the expression.
* @return a feature operation which computes its values using the given expression.
*
* @since 1.4
@@ -334,7 +336,7 @@ public final class FeatureOperations extends Static {
* @param <V> the type of values computed by the expression and assigned to the feature property.
* @param identification the name of the operation, together with optional information.
* @param expression the expression to evaluate on feature instances.
- * @param resultType type of values computed by the expression and assigned to the feature property.
+ * @param resultType type of values computed by the expression.
* @return a feature operation which computes its values using the given expression.
* @throws ClassCastException if the result type is not a target type supported by the expression.
*
@@ -346,4 +348,44 @@ public final class FeatureOperations extends Static {
{
return function(identification, expression.toValueType(resultType.getValueClass()), resultType);
}
+
+ /**
+ * Returns an expression for fetching the values of properties identified by the given type.
+ * The returned expression will be the first of the following choices which is applicable:
+ *
+ * <ul>
+ * <li>If the property is an expression built by {@link #expression expression(…)}, then the
+ * expression given to that method, or a derivative of that expression, is returned.</li>
+ * <li>If the property {@linkplain Features#getLinkTarget is a link},
+ * then a {@code ValueReference} fetching the link target is returned.</li>
+ * <li>Otherwise, a {@linkplain DefaultFilterFactory.Features#property value reference}
+ * is created for the name of the given property.</li>
+ * </ul>
+ *
+ * @param property the property for which to get an expression.
+ * @return an expression for fetching the values of the property identified by the given type.
+ * @since 1.5
+ */
+ public static Expression<? super Feature, ?> expressionOf(final PropertyType property) {
+ // Test final class first because it is fast.
+ if (property instanceof ExpressionOperation<?>) {
+ final Function<? super Feature, ?> expression = ((ExpressionOperation<?>) property).expression;
+ if (expression instanceof Expression<?,?>) {
+ return (Expression<? super Feature, ?>) expression;
+ }
+ }
+ String name;
+ final Class<?> type;
+ if (property instanceof AttributeType<?>) {
+ type = ((AttributeType<?>) property).getValueClass();
+ name = null;
+ } else {
+ type = Object.class;
+ name = Features.getLinkTarget(property).orElse(null);
+ }
+ if (name == null) {
+ name = property.getName().toString();
+ }
+ return DefaultFilterFactory.forFeatures().property(XPath.fromPropertyName(name), type);
+ }
}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
index aba5d401b8..c8ebf432b2 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/OperationWrapper.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.feature.builder;
+import java.util.Objects;
import org.opengis.util.GenericName;
import org.apache.sis.util.resources.Errors;
@@ -58,22 +59,33 @@ final class OperationWrapper extends PropertyTypeBuilder {
/**
* Do not allow a change of multiplicity.
*/
- @Override public PropertyTypeBuilder setMinimumOccurs(int occurs) {if (occurs == 1) return this; throw readOnly();}
- @Override public PropertyTypeBuilder setMaximumOccurs(int occurs) {if (occurs == 1) return this; throw readOnly();}
+ @Override public PropertyTypeBuilder setMinimumOccurs(int occurs) {return readOnly(1, occurs);}
+ @Override public PropertyTypeBuilder setMaximumOccurs(int occurs) {return readOnly(1, occurs);}
/**
* Do not allow modifications.
*/
- @Override public PropertyTypeBuilder setName (GenericName name) {throw readOnly();}
- @Override public PropertyTypeBuilder setDefinition (CharSequence definition) {throw readOnly();}
- @Override public PropertyTypeBuilder setDesignation(CharSequence designation) {throw readOnly();}
- @Override public PropertyTypeBuilder setDescription(CharSequence description) {throw readOnly();}
- @Override public PropertyTypeBuilder setDeprecated (boolean deprecated) {throw readOnly();}
+ @Override public PropertyTypeBuilder setName (GenericName name) {return readOnly(str(getName()), str(name));}
+ @Override public PropertyTypeBuilder setDefinition (CharSequence definition) {return readOnly(getDefinition(), definition);}
+ @Override public PropertyTypeBuilder setDesignation(CharSequence designation) {return readOnly(getDesignation(), designation);}
+ @Override public PropertyTypeBuilder setDescription(CharSequence description) {return readOnly(getDescription(), description);}
+ @Override public PropertyTypeBuilder setDeprecated (boolean deprecated) {return readOnly(isDeprecated(), deprecated);}
/**
- * Returns the exception to be thrown for read-only wrapper.
+ * Returns the string representation of the given name, or {@code null} if the name is null.
+ * This is used for relaxing the comparison of names before to throw an exception.
*/
- private UnsupportedOperationException readOnly() {
+ private static String str(final GenericName name) {
+ return (name != null) ? name.toString() : null;
+ }
+
+ /**
+ * Throws {@link UnsupportedOperationException} if the given value is different than the expected value.
+ */
+ private OperationWrapper readOnly(final Object expected, final Object actual) {
+ if (Objects.equals(expected, actual)) {
+ return this;
+ }
throw new UnsupportedOperationException(errors().getString(Errors.Keys.UnmodifiableObject_1, PropertyTypeBuilder.class));
}
}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/package-info.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/package-info.java
index 75ca037d52..b7fe070c00 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/package-info.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/package-info.java
@@ -93,7 +93,7 @@
* @author Travis L. Pinney
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
* @since 0.5
*/
package org.apache.sis.feature;
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
index b60a74b5e9..ae76ede67e 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
@@ -18,24 +18,26 @@ package org.apache.sis.feature.privy;
import java.util.Set;
import org.apache.sis.math.FunctionProperty;
+import org.apache.sis.util.UnconvertibleObjectException;
import org.apache.sis.filter.Optimization;
import org.apache.sis.filter.DefaultFilterFactory;
-import org.apache.sis.feature.builder.FeatureTypeBuilder;
-import org.apache.sis.feature.builder.PropertyTypeBuilder;
-import org.apache.sis.feature.builder.AttributeTypeBuilder;
import org.apache.sis.filter.internal.Node;
+import org.opengis.util.GenericName;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.FeatureType;
+import org.opengis.feature.Feature;
import org.opengis.feature.AttributeType;
+import org.opengis.feature.PropertyType;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.filter.InvalidFilterValueException;
import org.opengis.filter.Literal;
import org.opengis.filter.Expression;
import org.opengis.filter.ValueReference;
/**
- * OGC expressions or other functions operating on feature instances.
- * This interface adds an additional method, {@link #expectedType(FeatureType, FeatureTypeBuilder)},
+ * <abbr>OGC</abbr> expressions or other functions operating on feature instances.
+ * This interface adds an additional method, {@link #expectedType expectedType(…)},
* for fetching in advance the expected type of expression results.
*
* <p>This is an experimental interface which may be removed in any future version.</p>
@@ -73,56 +75,73 @@ public interface FeatureExpression<R,V> extends Expression<R,V> {
}
/**
- * Provides the expected type of values produced by this expression when a feature of the given
- * type is evaluated. The resulting type shall describe a "static" property, i.e. it can be an
- * {@link AttributeType} or a {@link org.opengis.feature.FeatureAssociationRole}
- * but not an {@link org.opengis.feature.Operation}.
+ * Provides the expected type of values produced by this expression when a feature of a given type is evaluated.
+ * Except for the special case of links (described below), the resulting type shall describe a "static" property,
+ * <i>i.e.</i> the type should be an {@link AttributeType} or a {@link org.opengis.feature.FeatureAssociationRole}
+ * but not an {@link org.opengis.feature.Operation}. The value of the static property will be set to the result of
+ * evaluating the expression when instances of the {@code FeatureType} will be created.
+ * This evaluation will be performed by {@link FeatureProjection#apply(Feature)}.
*
- * <p>If this method returns an instance of {@link AttributeTypeBuilder}, then its parameterized
- * type should be the same {@code <V>} than this {@code FeatureExpression}.</p>
+ * <h4>Implementation guideline</h4>
+ * Implementations should declare the property by invoking some of the following methods:
*
- * @param valueType the type of features to be evaluated by the given expression.
- * @param addTo where to add the type of properties evaluated by this expression.
- * @return builder of the added property, or {@code null} if this method cannot add a property.
- * @throws IllegalArgumentException if this method can operate only on some feature types
- * and the given type is not one of them.
+ * <ul>
+ * <li>{@link FeatureProjectionBuilder#source()} for the source of the {@link PropertyType} in next point.</li>
+ * <li>{@link FeatureProjectionBuilder#addSourceProperty(PropertyType, boolean)}</li>
+ * <li>{@link FeatureProjectionBuilder#addOptionalAttribute(String, Class)}</li>
+ * <li>{@link FeatureProjectionBuilder#addComputedAttribute(GenericName, Class)}</li>
+ * </ul>
+ *
+ * Inherited methods such as {@link FeatureProjectionBuilder#addAttribute(Class)} can also be invoked,
+ * but callers will be responsible for providing the value of the properties added by those methods.
+ * These values will not be provided by {@link FeatureProjection#apply(Feature)}.
+ *
+ * <h4>Operations</h4>
+ * If the property is a link to another property, such as {@code "sis:identifier"} or {@code "sis:geometry"},
+ * then adding this property may require the addition of dependencies. These dependencies will be detected by
+ * {@link FeatureProjectionBuilder}, which may generate an intermediate {@code FeatureType}.
+ *
+ * @param addTo where to add the type of the property evaluated by this expression.
+ * @return handler of the added property, or {@code null} if the property cannot be added.
+ * @throws InvalidFilterValueException if this expression is invalid for the requested operation.
+ * @throws PropertyNotFoundException if the property was not found in {@code addTo.source()}.
+ * @throws UnconvertibleObjectException if the property default value cannot be converted to the expected type.
*/
- PropertyTypeBuilder expectedType(FeatureType valueType, FeatureTypeBuilder addTo);
+ FeatureProjectionBuilder.Item expectedType(FeatureProjectionBuilder addTo);
/**
* Tries to cast or convert the given expression to a {@link FeatureExpression}.
- * If the given expression cannot be casted, then this method creates a copy
- * provided that the expression is one of the following type:
+ * If the given expression cannot be cast, then this method creates a copy
+ * if the expression is one of the following types:
*
* <ol>
* <li>{@link Literal}.</li>
- * <li>{@link ValueReference}, assuming that the expression expects feature instances.</li>
+ * <li>{@link ValueReference} if the given expression accepts feature instances.</li>
* </ol>
*
- * Otherwise this method returns {@code null}.
+ * Otherwise, this method returns {@code null}.
* It is caller's responsibility to verify if this method returns {@code null} and to throw an exception in such case.
- * We leave that responsibility to the caller because (s)he may be able to provide better error messages.
- *
- * <h4>Note on type safety</h4>
- * This method does not use parameterized types because of the assumption on {@link ValueReference}.
- * As of Apache SIS 1.3, we have no way to check if {@code <R>} is for feature instances.
+ * We leave that responsibility to the callers because they may be able to provide better error messages.
*
+ * @param <R> the type of resources used as inputs.
* @param candidate the expression to cast or copy. Can be null.
- * @return the given expression as a feature expression, or {@code null} if it cannot be casted or converted.
+ * @return the given expression as a feature expression, or {@code null} if it cannot be cast or converted.
*/
- public static FeatureExpression<?,?> castOrCopy(final Expression<?,?> candidate) {
+ public static <R> FeatureExpression<? super R, ?> castOrCopy(final Expression<R,?> candidate) {
if (candidate instanceof FeatureExpression<?,?>) {
- return (FeatureExpression<?,?>) candidate;
+ return (FeatureExpression<R,?>) candidate;
}
- final Expression<?,?> copy;
+ final Expression<? super R, ?> copy;
if (candidate instanceof Literal<?,?>) {
- copy = Optimization.literal(((Literal<?,?>) candidate).getValue());
+ copy = Optimization.literal(((Literal<R,?>) candidate).getValue());
} else if (candidate instanceof ValueReference<?,?>) {
- final String xpath = ((ValueReference<?,?>) candidate).getXPath();
- copy = DefaultFilterFactory.forFeatures().property(xpath);
+ final String xpath = ((ValueReference<R,?>) candidate).getXPath();
+ copy = DefaultFilterFactory.forResources(candidate.getResourceClass())
+ .map((factory) -> factory.property(xpath)).orElse(null);
} else {
return null;
}
- return (FeatureExpression<?,?>) copy;
+ // We do not expect a `ClassCastException` here because `copy` should be a SIS implementation.
+ return (FeatureExpression<? super R, ?>) copy;
}
}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjection.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjection.java
new file mode 100644
index 0000000000..b263c1e4dc
--- /dev/null
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjection.java
@@ -0,0 +1,341 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.feature.privy;
+
+import java.util.List;
+import java.util.Set;
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.Optional;
+import java.util.function.UnaryOperator;
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.privy.UnmodifiableArrayList;
+import org.apache.sis.filter.privy.ListingPropertyVisitor;
+import org.apache.sis.io.TableAppender;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.filter.Expression;
+import org.opengis.filter.ValueReference;
+
+
+/**
+ * A function applying projections (with "projection" in the <abbr>SQL</abbr> database sense) of features.
+ * Given full feature instances in input, the function returns feature instances containing only a subset
+ * of the properties. The property values may also be different if they are computed by the expressions.
+ *
+ * @author Guilhem Legal (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
+ */
+public final class FeatureProjection implements UnaryOperator<Feature> {
+ /**
+ * The type of features with the properties explicitly requested by the user.
+ * The property names may differ from the properties of the {@link FeatureProjectionBuilder#source() source}
+ * features if aliases were specified by calls to {@link FeatureProjectionBuilder.Item#setName(GenericName)}.
+ */
+ public final FeatureType typeRequested;
+
+ /**
+ * The requested type augmented with dependencies required for the execution of operations such as links.
+ * If there is no need for additional properties, then this value is the same as {@link #typeRequested}.
+ * The property names are the same as {@link #typeRequested} (i.e., may be aliases).
+ */
+ public final FeatureType typeWithDependencies;
+
+ /**
+ * Names of the properties to be stored in the feature instances created by this {@code FeatureProjection}.
+ * Properties that are computed on-the-fly from other properties are not included in this array. Properties
+ * that are present in {@link #typeRequested} but not in the source features are not in this array neither.
+ */
+ private final String[] propertiesToCopy;
+
+ /**
+ * Expressions to apply on the source feature for fetching the property values of the projected feature.
+ * This array has the same length as {@link #propertiesToCopy} and each expression is associated to the
+ * property at the same index.
+ */
+ private final Expression<? super Feature, ?>[] expressions;
+
+ /**
+ * Whether the {@link #apply(Feature)} method shall create instances of {@link #typeWithDependencies}.
+ * If {@code false}, then the instances given to the {@link #apply(Feature)} method will be assumed
+ * to be already instances of {@link #typeWithDependencies} and will be modified in-place.
+ */
+ private final boolean createInstance;
+
+ /**
+ * Creates a new projection with the given properties specified by a builder.
+ * The {@link #apply(Feature)} method will copy the properties of the given
+ * features into new instances of {@link #typeWithDependencies}.
+ *
+ * @param typeRequested the type of projected features.
+ * @param projection descriptions of the properties to keep in the projected features.
+ */
+ FeatureProjection(final FeatureType typeRequested, final FeatureType typeWithDependencies,
+ final List<FeatureProjectionBuilder.Item> projection)
+ {
+ this.createInstance = true;
+ this.typeRequested = typeRequested;
+ this.typeWithDependencies = typeWithDependencies;
+ int storedCount = 0;
+
+ // Expressions to apply on the source feature for fetching the property values of the projected feature.
+ @SuppressWarnings({"LocalVariableHidesMemberVariable", "unchecked", "rawtypes"})
+ final Expression<? super Feature,?>[] expressions = new Expression[projection.size()];
+
+ // Names of the properties to be stored in the attributes of the target features.
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
+ final String[] propertiesToCopy = new String[expressions.length];
+
+ for (final FeatureProjectionBuilder.Item item : projection) {
+ final var expression = item.attributeValueGetter();
+ if (expression != null) {
+ expressions[storedCount] = expression;
+ propertiesToCopy[storedCount++] = item.getName();
+ }
+ }
+ this.propertiesToCopy = ArraysExt.resize(propertiesToCopy, storedCount);
+ this.expressions = ArraysExt.resize(expressions, storedCount);
+ }
+
+ /**
+ * Creates a new projection with a subset of the properties of another projection.
+ * This constructor is invoked when the caller handles itself some of the properties.
+ *
+ * <h4>behavioral change</h4>
+ * Projections created by this constructor assumes that the feature instances given to the
+ * {@link #apply(Feature)} method are already instances of {@link #typeWithDependencies}
+ * and can be modified (if needed) in place. This constructor is designed for cases where
+ * the caller does itself a part of the {@code FeatureProjection} work.
+ *
+ * @param parent the projection from which to inherit the types and expressions.
+ * @param remaining index of the properties that still need to be copied after the caller did its processing.
+ *
+ * @see #afterPreprocessing(int[])
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private FeatureProjection(final FeatureProjection parent, final int[] remaining) {
+ createInstance = false;
+ typeRequested = parent.typeRequested;
+ typeWithDependencies = parent.typeWithDependencies;
+ expressions = new Expression[remaining.length];
+ propertiesToCopy = new String[remaining.length];
+ for (int i=0; i<remaining.length; i++) {
+ final int index = remaining[i];
+ propertiesToCopy[i] = parent.propertiesToCopy[index];
+ expressions[i] = parent.expressions[index];
+ }
+ }
+
+ /**
+ * Returns a variant of this projection where the caller has created the target feature instance itself.
+ * The callers is may have set some property values itself, and the {@code remaining} argument gives the
+ * indexes of the properties that still need to be copied after caller's processing.
+ *
+ * @param remaining index of the properties that still need to be copied after the caller did its processing.
+ * @return a variant of this projection which only completes the projection done by the caller,
+ * or {@code null} if there is nothing left to complete.
+ */
+ public FeatureProjection afterPreprocessing(final int[] remaining) {
+ if (remaining.length == 0 && typeRequested == typeWithDependencies) {
+ return null;
+ }
+ return new FeatureProjection(this, remaining);
+ }
+
+ /**
+ * Returns the names of all stored properties. This list may be shorter than the list of properties of the
+ * {@linkplain #typeRequested requested feature type} if some feature properties are computed on-the-fly,
+ * or if the target feature contains some new properties that are not in the source.
+ *
+ * @return the name of all stored properties.
+ */
+ public final List<String> propertiesToCopy() {
+ return UnmodifiableArrayList.wrap(propertiesToCopy);
+ }
+
+ /**
+ * Returns the path to the value (in source features) of the property at the given index.
+ * The argument corresponds to an index in the list returned by {@link #propertiesToCopy()}.
+ * The return value is often the same name as {@code propertiesToCopy().get(index)} but may
+ * differ if the user has specified aliases or if two properties have the same default name.
+ *
+ * @param index index of the stored property for which to get the name in the source feature.
+ * @return path in the source features, or empty if the property is not a {@link ValueReference}.
+ */
+ public Optional<String> xpath(final int index) {
+ final Expression<? super Feature, ?> expression = expressions[index];
+ if (expression instanceof ValueReference<?,?>) {
+ return Optional.of(((ValueReference<?,?>) expression).getXPath());
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Returns all dependencies used, directly or indirectly, by all expressions used in this projection.
+ * The set includes transitive dependencies (expressions with operands that are other expressions).
+ * The elements are XPaths.
+ *
+ * @return all dependencies (including transitive dependencies) as XPaths.
+ */
+ public Set<String> dependencies() {
+ Set<String> references = null;
+ for (var expression : expressions) {
+ references = ListingPropertyVisitor.xpaths(expression, references);
+ }
+ return (references != null) ? references : Set.of();
+ }
+
+ /**
+ * Derives a new projected feature instance from the given source.
+ * The feature type of the returned feature instance will be be {@link #typeRequested}.
+ * This method performs the following steps:
+ *
+ * <ol class="verbose">
+ * <li>If this projection was created by {@link #afterPreprocessing(int[])}, then the given feature
+ * shall be an instances of {@link #typeWithDependencies} and may be modified in-place. Otherwise,
+ * this method creates a new instance of {@link #typeWithDependencies}.</li>
+ * <li>This method executes all expressions for fetching values from {@code source}
+ * and stores the results in the feature instance of above step.</li>
+ * <li>If {@link #typeWithDependencies} is different than {@link #typeRequested}, then the feature
+ * of above step is wrapped in a view which hides the undesired properties.</li>
+ * </ol>
+ *
+ * @param source the source feature instance.
+ * @return the "projected" (<abbr>SQL</abbr> database sense) feature instance.
+ */
+ @Override
+ public Feature apply(final Feature source) {
+ var feature = createInstance ? typeWithDependencies.newInstance() : source;
+ for (int i=0; i < expressions.length; i++) {
+ feature.setPropertyValue(propertiesToCopy[i], expressions[i].apply(source));
+ }
+ if (typeRequested != typeWithDependencies) {
+ feature = new FeatureView(typeRequested, feature);
+ }
+ return feature;
+ }
+
+ /**
+ * Returns a string representation of this projection for debugging purposes.
+ * The current implementation formats a table with all properties, including
+ * dependencies, and a column saying whether the property is an operation,
+ * is stored or whether there is an error.
+ *
+ * @return a string representation.
+ */
+ @Override
+ public String toString() {
+ return Row.toString(this, propertiesToCopy);
+ }
+
+ /**
+ * Helper class for the implementation of {@link FeatureProjection#toString()}.
+ * Each instance represents a row in the table to be formatted..
+ * This is used for debugging purposes only.
+ */
+ @Debug
+ private static final class Row {
+ /**
+ * Returns a string representation of the {@link FeatureProjection} having the given values.
+ * Having this method in a separated class reduces the amount of classes loading, since this
+ * {@code Row} class is rarely needed in production environment.
+ *
+ * @param projection the projection for which to format a string representation.
+ * @param propertiesToCopy value of {@link FeatureProjection#propertiesToCopy}.
+ * @return the string representation of the given projection.
+ */
+ static String toString(final FeatureProjection projection, final String[] propertiesToCopy) {
+ final var rowByName = new LinkedHashMap<String,Row>();
+ if (projection.typeWithDependencies != projection.typeRequested) {
+ addAll(rowByName, projection.typeWithDependencies, "dependency");
+ }
+ addAll(rowByName, projection.typeRequested, "operation"); // Overwrite above dependencies.
+ for (int i=0; i < propertiesToCopy.length; i++) {
+ String name = propertiesToCopy[i];
+ String value;
+ try {
+ name = projection.typeWithDependencies.getProperty(name).getName().toString();
+ value = "stored";
+ } catch (RuntimeException e) {
+ value = e.toString();
+ }
+ Row row = rowByName.computeIfAbsent(name, Row::new);
+ row.type = value;
+ row.xpath = projection.xpath(i).orElse("");
+ }
+ final var words = Vocabulary.forLocale(null);
+ final var table = new TableAppender(" │ ");
+ table.setMultiLinesCells(true);
+ table.appendHorizontalSeparator();
+ table.append(words.getString(Vocabulary.Keys.Property)).nextColumn();
+ table.append(words.getString(Vocabulary.Keys.Type)).nextColumn();
+ table.append("XPath").nextLine();
+ table.appendHorizontalSeparator();
+ for (final Row row : rowByName.values()) {
+ table.append(row.property).nextColumn();
+ table.append(row.type).nextColumn();
+ table.append(row.xpath).nextLine();
+ }
+ table.appendHorizontalSeparator();
+ return table.toString();
+ }
+
+ /**
+ * Adds all properties of the given feature type into the specified map.
+ * For each property, {@link #type} is overwritten with the {@code type} argument value.
+ *
+ * @param rowByName where to add the properties.
+ * @param featureType the type from which to get the list of properties.
+ * @param type the "stored", "operation" or "dependency" value to assign to {@link #type}.
+ */
+ private static void addAll(final Map<String,Row> rowByName, final FeatureType featureType, final String type) {
+ for (final var property : featureType.getProperties(true)) {
+ Row row = rowByName.computeIfAbsent(property.getName().toString(), Row::new);
+ row.type = type;
+ }
+ }
+
+ /**
+ * Creates a row with no value.
+ *
+ * @param name number under which the property is stored.
+ */
+ private Row(final String name) {
+ property = name;
+ xpath = "";
+ }
+
+ /**
+ * Name of the property in the projected feature type.
+ */
+ private final String property;
+
+ /**
+ * Path to the property value in the source feature.
+ */
+ private String xpath;
+
+ /**
+ * Type: stored, operation or dependency.
+ */
+ private String type;
+ }
+}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
new file mode 100644
index 0000000000..f8a5c7a7c3
--- /dev/null
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
@@ -0,0 +1,682 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.feature.privy;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.List;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.UnaryOperator;
+import org.opengis.util.GenericName;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.feature.Features;
+import org.apache.sis.feature.AbstractOperation;
+import org.apache.sis.feature.FeatureOperations;
+import org.apache.sis.feature.builder.AttributeTypeBuilder;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.feature.builder.PropertyTypeBuilder;
+import org.apache.sis.util.privy.Strings;
+import org.apache.sis.util.resources.Vocabulary;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.PropertyType;
+import org.opengis.feature.IdentifiedType;
+import org.opengis.feature.Operation;
+import org.opengis.filter.Expression;
+import org.opengis.filter.ValueReference;
+
+
+/**
+ * A builder for deriving a feature type containing a subset of the properties of another type.
+ * The other type is called the {@linkplain #source() source} feature type. The properties that
+ * are retained may have different names than the property names of the source feature type.
+ * If a property is a link such as {@code sis:identifier} or {@code sis:geometry},
+ * this class keeps trace of the dependencies required for recreating the link.
+ *
+ * <p>Properties that are copied from the source feature type are declared by calls to
+ * {@link #addSourceProperty(PropertyType, boolean)} and related methods defined in this class.
+ * The methods inherited from the parent class can also be invoked,
+ * but they will receive no special treatment.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+public final class FeatureProjectionBuilder extends FeatureTypeBuilder {
+ /**
+ * The type of features that provide the values to store in the projected features.
+ * The value of this field does not change, except when following a XPath such as {@code "a/b/c"}.
+ *
+ * @see #source()
+ */
+ private FeatureType source;
+
+ /**
+ * Whether the source is a dependency of the feature type given to the constructor.
+ * This flag become {@code true} when following a XPath of the form {@code "a/b/c"}.
+ * In such case, {@link #source} may be temporarily set to the tip {@code "c"} type.
+ *
+ * @see #using(FeatureType, FeatureExpression)
+ */
+ private boolean sourceIsDependency;
+
+ /**
+ * The properties to inherit from the {@linkplain #source} feature type by explicit user's request.
+ * The property types and the property names are not necessarily the same as in the source feature.
+ * For example, an operation may be replaced by an attribute which will store the operation result.
+ *
+ * <h4>Implementation note</h4>
+ * This collection cannot be a {@link java.util.Map} with names of the source properties as keys,
+ * because the list may contain more than one item with the same {@link Item#sourceName} value.
+ * This collision happens if some items are the results of XPath evaluations such as {@code "a/b/c"}.
+ * This is the reason why properties can be renamed before to be stored in the projected features.
+ */
+ private final List<Item> requested;
+
+ /**
+ * Names that are actually used, or may be used, in the projected feature type.
+ * For each name, the associated value is the item that is explicitly using that name.
+ * A {@code null} value means that the name is not used, but is nevertheless reserved
+ * because potentially ambiguous. This information is used for avoiding name collisions
+ * in automatically generated names.
+ *
+ * <p>Note that the keys are not necessarily the values of {@link Item#sourceName}.
+ * Keys are rather the values of {@code Item.builder.getName()}, except that the
+ * latter may not be valid before {@link Item#validateName()} is invoked.</p>
+ *
+ * @see #reserve(GenericName, Item)
+ */
+ private final Map<GenericName, Item> reservedNames;
+
+ /**
+ * Whether at least one item has a name different than the property in the source feature type.
+ * If {@code true}, then the projection cannot be an {@linkplain #isIdentity() identity} operation.
+ * Result of operations may also need to be fetched in advance, because operations cannot be executed
+ * anymore after the name of a dependency changed.
+ *
+ * @see Item#setName(GenericName)
+ * @see #isIdentity()
+ */
+ private boolean hasRenamedItems;
+
+ /**
+ * Sequential number for generating default names of unnamed properties. This is used when the
+ * name inherited from the {@linkplain #source} feature is unknown or collides with another name.
+ */
+ private int unnamedNumber;
+
+ /**
+ * Names of {@linkplain #source} properties that are dependencies found in operations.
+ * The most common cases are the targets of {@code "sis:identifier"} and {@code "sis:geometry"} links.
+ * Values are the items having this dependency. Many items may share the same dependency.
+ *
+ * <p>At first, this map is populated without checking if the properties requested by the user contains
+ * those dependencies. After all user's requested properties have been declared to this builder, values
+ * are filtered for identifying which dependencies need to be added as implicit properties.</p>
+ */
+ private final Map<String, List<Item>> dependencies;
+
+ /**
+ * Creates a new builder instance using the default factories.
+ *
+ * @todo provides a way to specify the factories used by the data store.
+ *
+ * @param source the type from which to take the properties to keep in the projected feature.
+ * @param locale the locale to use for formatting error messages, or {@code null} for the default locale.
+ */
+ public FeatureProjectionBuilder(final FeatureType source, final Locale locale) {
+ super(null, null, locale);
+ this.source = Objects.requireNonNull(source);
+ requested = new ArrayList<>();
+ dependencies = new HashMap<>();
+ reservedNames = new HashMap<>();
+ }
+
+ /**
+ * Returns the type of features that provide the values to store in the projected features.
+ * This is the type given at construction time, except when following a XPath such as
+ * {@code "a/b/c"} in which case it may temporarily be the leaf {@code "c"} type.
+ *
+ * @return the current source of properties (never {@code null}).
+ */
+ public FeatureType source() {
+ return source;
+ }
+
+ /**
+ * Returns the expected type of the given expression using the given feature type as the source.
+ * This method temporarily sets the {@linkplain #source() source} to the given {@code childType},
+ * then returns the value of {@code expression.expectedType(this)}.
+ * This is used for the last expression in a XPath such as {@code "a/b/c"}.
+ *
+ * @param childType the feature type to use.
+ * @param expression the expression from which to get the expected type.
+ * @return the expected type, or {@code null}.
+ *
+ * @see FeatureExpression#expectedType(FeatureProjectionBuilder)
+ */
+ public Item using(final FeatureType childType, final FeatureExpression<?,?> expression) {
+ final FeatureType previous = source;
+ final boolean status = sourceIsDependency;
+ try {
+ sourceIsDependency = true;
+ source = Objects.requireNonNull(childType);
+ return expression.expectedType(this);
+ } finally {
+ source = previous;
+ sourceIsDependency = status;
+ }
+ }
+
+ /**
+ * Adds the given property, replacing operation by an attribute storing the operation result.
+ * This method may return {@code null} if it cannot resolve the property type, in which case
+ * the caller should throw an exception (throwing an exception is left to the caller because
+ * it can produces a better error message). Operation's dependencies, if any, are added into
+ * the given {@code deferred} list.
+ *
+ * @param property the {@linkplain #source} property to add.
+ * @param deferred where to add operation's dependencies, or {@code null} for not collecting dependencies.
+ * @return builder for the projected property, or {@code null} if it cannot be resolved.
+ */
+ private PropertyTypeBuilder addPropertyResult(PropertyType property, final List<String> deferred) {
+ if (property instanceof Operation) {
+ final GenericName name = property.getName();
+ do {
+ if (deferred != null) {
+ if (property instanceof AbstractOperation) {
+ deferred.addAll(((AbstractOperation) property).getDependencies());
+ } else {
+ /*
+ * Cannot resolve dependencies. Current implementation assumes that there is no dependency.
+ * Note: we could conservatively add all properties as dependencies, but this is difficult
+ * to implement efficiently.
+ */
+ }
+ }
+ final IdentifiedType result = ((Operation) property).getResult();
+ if (result != property && result instanceof PropertyType) {
+ property = (PropertyType) result;
+ } else if (result instanceof FeatureType) {
+ return addAssociation((FeatureType) result).setName(name);
+ } else {
+ return null;
+ }
+ } while (property instanceof Operation);
+ return addProperty(property).setName(name);
+ }
+ return addProperty(property);
+ }
+
+ /**
+ * Adds a property from the source feature type. The given property should be the result of a call to
+ * {@code source().getProperty(sourceName)}. The call to {@code getProperty(…)} is left to the caller
+ * because some callers need to wrap that call in a {@code try} block.
+ *
+ * @param property the property type, usually as one of the properties of {@link #source()}.
+ * @param named whether the {@code property} name can be used as a default name.
+ * @return handler for the given item, or {@code null} if the given property cannot be resolved.
+ */
+ public Item addSourceProperty(final PropertyType property, final boolean named) {
+ final PropertyTypeBuilder builder;
+ List<String> deferred;
+ if (sourceIsDependency) {
+ /*
+ * Adding a property which is not defined in the feature type specified at construction time,
+ * but which is defined at the tip of some XPath such as "a/b/c". This is not the same thing
+ * as adding an association. This is rather adding a subset of an association. We do not add
+ * dependency information because the dependencies are not directly in the source feature.
+ */
+ reserve(property.getName(), null);
+ deferred = new ArrayList<>();
+ builder = addPropertyResult(property, deferred);
+ } else {
+ /*
+ * For link operations, remember the dependencies in order to determine (after we added all properties)
+ * if we can keep the property as an operation or if we will need to copy the value in an attribute.
+ * For other kind of operations, unconditionally replace the operation by its result.
+ */
+ deferred = Features.getLinkTargets(property);
+ if (deferred.isEmpty()) {
+ deferred = new ArrayList<>();
+ builder = addPropertyResult(property, deferred);
+ } else {
+ builder = addProperty(property);
+ }
+ }
+ final var item = new Item(named ? property.getName() : null, builder);
+ requested.add(item);
+ for (String dependency : deferred) {
+ dependencies.computeIfAbsent(dependency, (key) -> new ArrayList<>(2)).add(item);
+ }
+ return item;
+ }
+
+ /**
+ * Adds an optional attribute type for values of the specified class.
+ * This method can be invoked as an alternative to {@link #addSourceProperty(PropertyType, boolean)}
+ * when the source property cannot be found and a fallback is added instead.
+ *
+ * @param name the name in the source feature.
+ * @param type the class of attribute values.
+ * @return handler for the given item.
+ */
+ public Item addOptionalAttribute(final String name, final Class<?> type) {
+ final AttributeTypeBuilder<?> builder = addAttribute(type).setMinimumOccurs(0);
+ final var item = new Item(builder.setName(name).getName(), builder);
+ requested.add(item);
+ return item;
+ }
+
+ /**
+ * Adds a mandatory attribute type for values of the specified class.
+ * The attribute is assumed to be computed by a function of the given name.
+ * This is usually not the name of a property in the {@linkplain #source} feature.
+ *
+ * @param name name of the function computing the attribute value.
+ * @param type the class of attribute values.
+ * @return handler for the given item.
+ */
+ public Item addComputedAttribute(final GenericName name, final Class<?> type) {
+ final var item = new Item(null, addAttribute(type).setName(name));
+ requested.add(item);
+ return item;
+ }
+
+ /**
+ * Handler for a property inherited from the source feature type. The property is initially unnamed.
+ * A name can be specified explicitly after construction by a call to {@link #setName(GenericName)}.
+ * If no name is specified, the default name will be the same as in the source feature type if that
+ * name is available, or a default name otherwise.
+ */
+ public final class Item {
+ /**
+ * The name that the property had in the {@linkplain #source() source} feature, or {@code null}.
+ * The property built by the {@linkplain #builder} will often have the same name, but not always.
+ */
+ final GenericName sourceName;
+
+ /**
+ * The builder for configuring the property.
+ */
+ private PropertyTypeBuilder builder;
+
+ /**
+ * Whether this item got an explicit name. The specified name may be
+ * identical to the name in the {@linkplain #source() source} feature.
+ */
+ private boolean isNamed;
+
+ /**
+ * Whether to keep the current name if it is available. This is set to {@code true} when user did not
+ * specified explicitly a name, but keeping the name of the source property would be a natural choice.
+ * However, before to use that name, we need to wait and see if that name will be explicitly used for
+ * another property.
+ */
+ private boolean preferCurrentName;
+
+ /**
+ * Whether this property needs at least one dependency which is not included in the list of properties
+ * requested by the user. In such case, we cannot keep the link operation and need to replace the link
+ * by a stored attribute.
+ *
+ * @see #replaceIfMissingDependency()
+ */
+ private boolean hasMissingDependency;
+
+ /**
+ * Expression for evaluating the attribute value from a source feature instance, or {@code null} if none.
+ * This field should be non-null only if the value will be stored in an attribute. If the property is an
+ * operation, then this field should be null (this is not the expression of the operation).
+ *
+ * @see #attributeValueGetter()
+ */
+ private Expression<? super Feature, ?> attributeValueGetter;
+
+ /**
+ * Creates a new handle for the property created by the given builder.
+ *
+ * @param sourceName the property name in the {@linkplain #source() source} feature, or {@code null}.
+ * @param builder the builder for configuring the property.
+ */
+ private Item(final GenericName sourceName, final PropertyTypeBuilder builder) {
+ this.sourceName = sourceName;
+ this.builder = builder;
+ }
+
+ /**
+ * Returns a string representation for debugging purposes.
+ */
+ @Override
+ public String toString() {
+ return Strings.toString(getClass(),
+ "sourceName", (sourceName != null) ? sourceName.toString() : null,
+ "targetName", isNamed ? getName() : null,
+ "valueClass", (builder instanceof AttributeTypeBuilder<?>) ? ((AttributeTypeBuilder<?>) builder).getValueClass() : null,
+ null, hasMissingDependency ? "hasMissingDependency" : null);
+ }
+
+ /**
+ * Replaces this property by a stored attribute if at least one dependency is not in the list of properties
+ * requested by the user. This method should be invoked only for preparing the user requested feature type.
+ * This method should not be invoked for preparing the feature type with dependencies, because the latter
+ * should contain the missing dependencies.
+ */
+ private void replaceIfMissingDependency() {
+ if (hasMissingDependency) {
+ hasMissingDependency = false;
+ final var old = builder;
+ builder = addPropertyResult(old.build(), null); // `old.build()` returns the existing operation.
+ old.replaceBy(builder);
+ }
+ }
+
+ /**
+ * Sets the class of attribute values. If the builder is an instance of {@link AttributeTypeBuilder}
+ * and if {@code type.apply(valueClass)} returns a non-null value ({@code valueClass} is the current
+ * class of attribute values), then this method sets the new attribute value class to the specified
+ * type and returns {@code true}. Otherwise, this method returns {@code false}.
+ *
+ * @param type a converter from current class to the new class of attribute values.
+ * @return whether the value class has been set to the value returned by {@code type}.
+ * @throws UnconvertibleObjectException if the default value cannot be converted to the given type.
+ */
+ public boolean replaceValueClass(final UnaryOperator<Class<?>> type) {
+ if (builder instanceof AttributeTypeBuilder<?>) {
+ final var ab = (AttributeTypeBuilder<?>) builder;
+ final Class<?> r = type.apply(ab.getValueClass());
+ if (r != null) {
+ builder = ab.setValueClass(r);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the expression to use for evaluating the property value.
+ * If {@code stored} is {@code true} (the usual case), then the expression will be evaluated early
+ * and its result will be stored as an attribute value, unless this property is not an attribute.
+ * If {@code stored} is {@code false}, this method replaces the attribute by an operation wrapping
+ * the given expression. In other words, the evaluation of the expression will be deferred.
+ * The latter case is possible only if the {@code FeatureType} contains all dependencies
+ * that the operation needs.
+ *
+ * @param expression the expression to be evaluated by the operation.
+ */
+ public void setValueGetter(final Expression<? super Feature, ?> expression, final boolean stored) {
+ if (builder instanceof AttributeTypeBuilder<?>) {
+ if (stored) {
+ attributeValueGetter = expression;
+ } else {
+ final var atb = (AttributeTypeBuilder<?>) builder;
+ /*
+ * Optimization: we could compute `storedType = atb.build()` unconditionally,
+ * which creates an attribute with the final name in the target feature type.
+ * However, in the particular case of links, we are better to use the name of
+ * the property in the source feature type, because it allows an optimization
+ * in `ExpressionOperation.create(…)` (a replacement by a `LinkOperation`).
+ */
+ AttributeType<?> storedType = null;
+ if (expression instanceof ValueReference<?,?>) {
+ var candidate = source.getProperty(((ValueReference<?,?>) expression).getXPath());
+ if (candidate instanceof AttributeType<?>) {
+ storedType = (AttributeType<?>) candidate;
+ }
+ }
+ if (storedType == null) {
+ storedType = atb.build(); // Same name as in the `identification` map below.
+ }
+ final var identification = Map.of(AbstractOperation.NAME_KEY, builder.getName());
+ builder = addProperty(FeatureOperations.expression(identification, expression, storedType));
+ atb.replaceBy(builder);
+ }
+ } else {
+ // The property is an operation, usually a link. Leave it as-is.
+ }
+ }
+
+ /**
+ * Returns the expression for evaluating the value to store in the attribute built by this item.
+ * The expression may be {@code null} if the value is computed on-the-fly (i.e. the property is
+ * an operation), or if the expression has not been specified.
+ */
+ final Expression<? super Feature, ?> attributeValueGetter() {
+ return attributeValueGetter;
+ }
+
+ /**
+ * Sets the coordinate reference system that characterizes the values of this attribute.
+ *
+ * @param crs coordinate reference system associated to attribute values, or {@code null}.
+ * @return {@code this} for method calls chaining.
+ */
+ public Item setCRS(final CoordinateReferenceSystem crs) {
+ if (builder instanceof AttributeTypeBuilder<?>) {
+ builder = ((AttributeTypeBuilder<?>) builder).setCRS(crs);
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether the property built by this item is equivalent to the given property.
+ * The current implementation bases its test only on the property name.
+ * This is reliable only if {@link #hasRenamedItems} is {@code false}.
+ *
+ * @param property the property to compare.
+ * @return whether this item builds a property equivalent to the given one.
+ *
+ * @see #isIdentity()
+ */
+ private boolean equivalent(final PropertyType property) {
+ return builder.getName().equals(property.getName());
+ }
+
+ /**
+ * Returns the name of the projected property.
+ * This is initially the name of the property given at construction time,
+ * but can be changed later by a call to {@link #setName(GenericName)}.
+ *
+ * @return the name of the projected property.
+ */
+ public String getName() {
+ return builder.getName().toString();
+ }
+
+ /**
+ * Sets the name of the projected property. A {@code null} argument means that the name is unspecified,
+ * in which case a different name may be generated later if the current name collides with other names.
+ *
+ * <p>This method should be invoked exactly once for each item, even if the argument is {@code null}.
+ * The reason is because this method uses this information for recording which names to reserve.</p>
+ *
+ * @param targetName the desired name in the projected feature, or {@code null} if unspecified.
+ */
+ public void setName(final GenericName targetName) {
+ if (targetName == null) {
+ reserve(sourceName, null); // Will use that name only if not owned by another item.
+ preferCurrentName = true;
+ } else if (targetName.equals(sourceName)) {
+ reserve(sourceName, this); // Take possession of that name.
+ isNamed = true;
+ } else {
+ builder.setName(targetName);
+ reserve(targetName, this);
+ hasRenamedItems = true;
+ isNamed = true;
+ }
+ }
+
+ /**
+ * If this item has not received an explicit name, infers a default name.
+ * This method should be invoked only after {@link #setName(GenericName)}
+ * has been invoked for all items, for allowing this class to know which
+ * names are reserved.
+ */
+ private void validateName() {
+ if (!isNamed) {
+ final Item owner = reservedNames.get(sourceName);
+ if (owner != this) {
+ GenericName name = sourceName;
+ if (owner != null || name == null || (!preferCurrentName && reservedNames.containsKey(name))) {
+ do {
+ var text = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed_1, ++unnamedNumber);
+ name = builder.setName(text).getName(); // Local name with the appropriate name space.
+ } while (reservedNames.containsKey(name)); // Reminder: the associated value may be null.
+ }
+ reserve(name, this);
+ }
+ isNamed = true;
+ }
+ }
+ }
+
+ /**
+ * Declares the given name as reserved. If this class needs to generate a default name,
+ * it will ensure that automatically generated names do not conflict with reserved names.
+ *
+ * @param name name to reserve for a projected property type, or {@code null} if none.
+ * @param owner the builder using that name, or {@code null} if none.
+ */
+ private void reserve(GenericName name, final Item owner) {
+ if (name != null) {
+ // By `putIfAbsent` method contract, non-null values have precedence over null values.
+ reservedNames.putIfAbsent(name, owner);
+ if (name != (name = name.tip())) { // Shortcut for a majority of cases.
+ reservedNames.putIfAbsent(name, owner);
+ }
+ }
+ }
+
+ /**
+ * Adds dependencies. This method adds in the {@code deferred} list any transitive
+ * dependencies which may need to be added in a second pass after this method call.
+ * The elements added into {@code deferred} are {@linkplain #source} properties.
+ *
+ * @param deferred where to add missing transitive dependencies (source properties).
+ */
+ private void resolveDependencies(final List<PropertyType> deferred) {
+ final var it = dependencies.entrySet().iterator();
+ while (it.hasNext()) {
+ final Map.Entry<String, List<Item>> entry = it.next();
+ final PropertyType property = source.getProperty(entry.getKey());
+ final GenericName sourceName = property.getName();
+ Item item = reservedNames.get(sourceName);
+ if (item != null) {
+ if (!sourceName.equals(item.sourceName)) {
+ /*
+ * If we want to support that feature in a future version, we would need a `replace` method
+ * for replacing a builder at a specific index or for a specific property name. A difficulty
+ * is that for compound identifiers, we have no API for reusing the same prefix and suffix.
+ */
+ throw new UnsupportedOperationException("Renaming of properties used in links is not yet supported.");
+ }
+ } else {
+ for (Item dependent : entry.getValue()) {
+ dependent.hasMissingDependency = true;
+ }
+ deferred.add(property);
+ }
+ it.remove();
+ }
+ }
+
+ /**
+ * Returns {@code true} if the feature to be built should be equivalent to the source feature.
+ *
+ * @return whether the {@linkplain #source} feature type can be used directly.
+ */
+ private boolean isIdentity() {
+ if (hasRenamedItems) {
+ return false;
+ }
+ final Iterator<Item> it = requested.iterator();
+ for (PropertyType property : source.getProperties(true)) {
+ if (!(it.hasNext() && it.next().equivalent(property))) {
+ return false;
+ }
+ }
+ return !it.hasNext();
+ }
+
+ /**
+ * Returns the feature type described by this builder. This method may return the
+ * {@linkplain #source() source} directly if this projection performs no operation.
+ */
+ @Override
+ public FeatureType build() {
+ return isIdentity() ? source : super.build();
+ }
+
+ /**
+ * Sets the default name of all anonymous properties, then builds the feature types.
+ * Two feature types are built: one with only the requested properties, and another
+ * type augmented with dependencies of operations such as links.
+ *
+ * <p>This method should be invoked exactly once.</p>
+ *
+ * <h4>Identity operation</h4>
+ * If the result is a feature type with all the properties of the source feature,
+ * with the same property names in the same order, and if the expressions are only
+ * fetching the values (no computation), then this method returns an empty value
+ * for meaning that this projection does nothing.
+ *
+ * @return the feature types with and without dependencies, or empty if there is no projection.
+ */
+ public Optional<FeatureProjection> project() {
+ requested.forEach(Item::validateName);
+ /*
+ * Add properties for all dependencies that are required by link operations but are not already present.
+ * If there is no need to add anything, `typeWithDependencies` will be directly the feature type to return.
+ */
+ final List<PropertyTypeBuilder> properties = properties();
+ final int count = properties.size();
+ final var deferred = new ArrayList<PropertyType>();
+ resolveDependencies(deferred);
+ /*
+ * If there is no dependencies, the requested type and the type with dependencies are the same.
+ * Otherwise, we need to resolve transitive dependencies before to build each type.
+ */
+ final FeatureType typeRequested, typeWithDependencies;
+ if (deferred.isEmpty()) {
+ typeRequested = typeWithDependencies = build();
+ } else {
+ do {
+ for (PropertyType property : deferred) {
+ final Item item = addSourceProperty(property, true);
+ item.validateName();
+ item.setValueGetter(FeatureOperations.expressionOf(property), true);
+ }
+ deferred.clear();
+ resolveDependencies(deferred);
+ } while (!deferred.isEmpty());
+ typeWithDependencies = build();
+ properties.subList(count, properties.size()).clear(); // Keep only the properties requested by user.
+ requested.forEach(Item::replaceIfMissingDependency);
+ typeRequested = build();
+ }
+ if (source.equals(typeRequested) && source.equals(typeWithDependencies)) {
+ return Optional.empty();
+ }
+ return Optional.of(new FeatureProjection(typeRequested, typeWithDependencies, requested));
+ }
+}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureView.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureView.java
new file mode 100644
index 0000000000..a0dac0a57f
--- /dev/null
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureView.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.feature.privy;
+
+import org.apache.sis.feature.AbstractFeature;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.Property;
+
+
+/**
+ * A feature containing a subset of the properties of another feature.
+ * The feature type of the view must be specified in argument together with the source feature instance.
+ * All properties that are present in this feature view shall have the same name as in the source feature.
+ * This class does not verify that requirement.
+ *
+ * <h2>Limitations</h2>
+ * For performance and simplicity reasons, the current implementation does not prevent users
+ * from requesting a property that exists in the full feature instance but not in this subset.
+ *
+ * <h2>Possible evolution</h2>
+ * It would be possible for the view to contain operations that are not present in the source features.
+ * This extension has not yet been implemented because not yet needed.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+final class FeatureView extends AbstractFeature {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = -4299168913599597991L;
+
+ /**
+ * The instance with all properties.
+ */
+ @SuppressWarnings("serial") // Apache SIS implementations are serializable.
+ private final Feature source;
+
+ /**
+ * Creates a new feature instance which is a subset of the given {@code source} feature.
+ * This constructor does not verify the consistency of the two arguments.
+ *
+ * @param subset the feature type that describes the feature subset.
+ * @param source the complete feature instance to view as a subset.
+ */
+ FeatureView(final FeatureType subset, final Feature source) {
+ super(subset);
+ this.source = source;
+ }
+
+ /**
+ * Returns the property (attribute, feature association or operation result) of the given name.
+ * This method delegates to the wrapped source without checking whether the given name exists in this subset.
+ *
+ * @param name the property name.
+ * @return the property of the given name (never {@code null}).
+ */
+ @Override
+ public Property getProperty(final String name) {
+ return source.getProperty(name);
+ }
+
+ /**
+ * Sets the property (attribute or feature association).
+ * This method delegates to the wrapped source without checking whether the given name exists in this subset.
+ *
+ * @param property the property to set.
+ */
+ @Override
+ public void setProperty(final Property property) {
+ source.setProperty(property);
+ }
+
+ /**
+ * Returns the value for the property of the given name.
+ * This method delegates to the wrapped source without checking whether the given name exists in this subset.
+ *
+ * @param name the property name.
+ * @return value of the specified property, or the default value (which may be {@code null}} if none.
+ */
+ @Override
+ public Object getPropertyValue(final String name) {
+ return source.getPropertyValue(name);
+ }
+
+ /**
+ * Sets the value for the property of the given name.
+ * This method delegates to the wrapped source without checking whether the given name exists in this subset.
+ *
+ * @param name the property name.
+ * @param value the new value for the specified property (may be {@code null}).
+ */
+ @Override
+ public void setPropertyValue(final String name, final Object value) {
+ source.setPropertyValue(name, value);
+ }
+
+ /**
+ * Returns the value for the property of the given name if that property exists, or a fallback value otherwise.
+ * This method delegates to the wrapped source without checking whether the given name exists in this subset.
+ *
+ * <h4>Design note</h4>
+ * We could add a verification of whether the property exists in the feature type given by {@link #getType()}.
+ * We don't do that for now because the current usages of this method in the Apache SIS code base do not need
+ * this method to be strict, and for consistency with the behavior of other methods in this class.
+ *
+ * @param name the property name.
+ * @param missingPropertyFallback the value to return if no attribute or association of the given name exists.
+ * @return value or default value of the specified property, or {@code missingPropertyFallback}.
+ */
+ @Override
+ public Object getValueOrFallback(final String name, final Object missingPropertyFallback) {
+ return source.getValueOrFallback(name, missingPropertyFallback);
+ }
+}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
index f5a9c9a2ed..eabd8cdf58 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
@@ -19,9 +19,8 @@ package org.apache.sis.filter;
import java.math.BigDecimal;
import java.math.BigInteger;
import org.opengis.util.ScopedName;
-import org.apache.sis.feature.builder.FeatureTypeBuilder;
-import org.apache.sis.feature.builder.PropertyTypeBuilder;
import org.apache.sis.feature.privy.FeatureExpression;
+import org.apache.sis.feature.privy.FeatureProjectionBuilder;
import org.apache.sis.filter.privy.FunctionNames;
import org.apache.sis.util.UnconvertibleObjectException;
import org.apache.sis.util.resources.Errors;
@@ -29,7 +28,6 @@ import org.apache.sis.math.Fraction;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.AttributeType;
-import org.opengis.feature.FeatureType;
import org.opengis.filter.Expression;
@@ -88,8 +86,8 @@ abstract class ArithmeticFunction<R> extends BinaryFunction<R,Number,Number>
* on the {@code ArithmeticFunction} subclass and is given by {@link #expectedType()}.
*/
@Override
- public final PropertyTypeBuilder expectedType(FeatureType ignored, FeatureTypeBuilder addTo) {
- return addTo.addProperty(expectedType());
+ public final FeatureProjectionBuilder.Item expectedType(FeatureProjectionBuilder addTo) {
+ return addTo.addSourceProperty(expectedType(), false);
}
/**
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
index 5597c0dc3e..1ab716a0a3 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
@@ -22,8 +22,7 @@ import java.util.List;
import java.util.Collection;
import java.util.Optional;
import org.apache.sis.feature.Features;
-import org.apache.sis.feature.builder.FeatureTypeBuilder;
-import org.apache.sis.feature.builder.PropertyTypeBuilder;
+import org.apache.sis.feature.privy.FeatureProjectionBuilder;
import org.apache.sis.math.FunctionProperty;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -38,8 +37,8 @@ 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 XPath such as "a/b/c". The last element of the path
- * (the tip) is evaluated by a {@link PropertyValue}.
+ * This is used for value reference given by a limited form of XPath such as {@code "a/b/c"}.
+ * The last element of the path (the tip) is evaluated by a {@link PropertyValue}.
*
* @author Martin Desruisseaux (Geomatys)
*
@@ -202,15 +201,16 @@ walk: if (specifiedType != null) try {
}
/**
- * Provides the expected type of values produced by this expression when a feature of the given type is evaluated.
+ * Provides the expected type of values produced by this expression. This method delegates to
+ * {@link #accessor} after setting the source feature type to the tip of {@code "a/b/c"} path.
*
- * @param valueType the type of features to be evaluated by the given expression.
- * @param addTo where to add the type of properties evaluated by the given expression.
+ * @param addTo where to add the type of properties evaluated by this expression.
* @return builder of the added property, or {@code null} if this method cannot add a property.
- * @throws IllegalArgumentException if this method cannot determine the property type for the given feature type.
+ * @throws PropertyNotFoundException if the property was not found in {@code addTo.source()}.
*/
@Override
- public PropertyTypeBuilder expectedType(FeatureType valueType, final FeatureTypeBuilder addTo) {
+ public FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
+ FeatureType valueType = addTo.source();
for (final String p : path) {
final PropertyType type;
try {
@@ -218,7 +218,7 @@ walk: if (specifiedType != null) try {
} 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);
+ return accessor.defaultType(addTo);
}
throw e;
}
@@ -227,7 +227,7 @@ walk: if (specifiedType != null) try {
}
valueType = ((FeatureAssociationRole) type).getValueType();
}
- return accessor.expectedType(valueType, addTo);
+ return addTo.using(valueType, accessor);
}
/**
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
index 3e9d2f3110..c614215ed9 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
@@ -23,16 +23,13 @@ import org.opengis.util.ScopedName;
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.feature.privy.FeatureExpression;
+import org.apache.sis.feature.privy.FeatureProjectionBuilder;
import org.apache.sis.math.FunctionProperty;
import org.apache.sis.util.resources.Errors;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.filter.Expression;
-import org.opengis.feature.FeatureType;
/**
@@ -166,20 +163,20 @@ final class ConvertFunction<R,S,V> extends UnaryFunction<R,S>
}
/**
- * Provides the type of values produced by this expression when a feature of the given type is evaluated.
+ * Provides the type of values produced by this expression.
* May return {@code null} if the type cannot be determined.
+ *
+ * @throws UnconvertibleObjectException if the property default value cannot be converted to the expected type.
*/
@Override
- public PropertyTypeBuilder expectedType(final FeatureType valueType, final FeatureTypeBuilder addTo) {
+ public FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(expression);
if (fex == null) {
return null;
}
- final PropertyTypeBuilder p = fex.expectedType(valueType, addTo);
- if (p instanceof AttributeTypeBuilder<?>) {
- return ((AttributeTypeBuilder<?>) p).setValueClass(getValueClass());
- }
- return p;
+ final FeatureProjectionBuilder.Item item = fex.expectedType(addTo);
+ item.replaceValueClass((c) -> getValueClass());
+ return item;
}
/**
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
index c020754996..9133bec01e 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
@@ -20,6 +20,7 @@ import java.util.Map;
import java.util.HashMap;
import java.util.Collection;
import java.util.ServiceLoader;
+import java.util.Optional;
import javax.measure.Quantity;
import javax.measure.quantity.Length;
import org.opengis.geometry.Envelope;
@@ -138,6 +139,30 @@ public abstract class DefaultFilterFactory<R,G,T> extends AbstractFactory implem
return Features.DEFAULT;
}
+ /**
+ * Returns a factory operating on resource instances of the given class.
+ * The current implementation recognizes the following classes:
+ *
+ * <ul>
+ * <li>{@link Feature}: delegate to {@link #forFeatures()}.</li>
+ * </ul>
+ *
+ * More classes may be added in future versions.
+ *
+ * @param <R> compile-time value of the {@code type} argument.
+ * @param type type of resources that the factory shall accept.
+ * @return factory operating on resource instances of the given class.
+ * @since 1.5
+ */
+ @SuppressWarnings("unchecked")
+ public static <R> Optional<FilterFactory<R, Object, Object>> forResources(final Class<R> type) {
+ if (type.equals(Feature.class)) {
+ return Optional.of((FilterFactory<R, Object, Object>) forFeatures());
+ } else {
+ return Optional.empty();
+ }
+ }
+
/**
* Describes the abilities of this factory. The description includes restrictions on
* the available spatial operations, scalar operations, lists of supported functions,
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
new file mode 100644
index 0000000000..3a49222670
--- /dev/null
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.filter.InvalidFilterValueException;
+
+
+/**
+ * Exceptions thrown when the XPath in an expression is invalid or unsupported.
+ * Apache SIS currently supports only a small subset of XPath syntax, mostly paths of
+ * the form {@code "a/b/c"} (and not everywhere) and the {@code "Q{namespace}"} syntax.
+ *
+ * <h4>Relationship with standard libraries</h4>
+ * The standard Java libraries provides a {@link javax.xml.xpath.XPathException}.
+ * This {@code InvalidXPathException} differs in that it is an unchecked exception
+ * and is thrown in the context of operations with OGC filters and expressions.
+ * In some implementations, {@code InvalidXPathException} may have a {@code XPathException} as its cause.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ *
+ * @see javax.xml.xpath.XPathException
+ *
+ * @since 1.5
+ */
+public class InvalidXPathException extends InvalidFilterValueException {
+ /**
+ * Serial number for inter-operability with different versions.
+ */
+ private static final long serialVersionUID = 1654277877397802378L;
+
+ /**
+ * Creates an exception with no message.
+ */
+ public InvalidXPathException() {
+ super();
+ }
+
+ /**
+ * Creates an exception with the specified message.
+ *
+ * @param message the detail message, saved for later retrieval by the {@link #getMessage()} method.
+ */
+ public InvalidXPathException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates an exception with the specified cause.
+ *
+ * @param cause the cause, saved for later retrieval by the {@link #getCause()} method.
+ */
+ public InvalidXPathException(final Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Creates an exception with the specified message and cause.
+ *
+ * @param message the detail message, saved for later retrieval by the {@link #getMessage()} method.
+ * @param cause the cause, saved for later retrieval by the {@link #getCause()} method.
+ */
+ public InvalidXPathException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
index 308c9adc7c..a869d79ae4 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
@@ -27,13 +27,11 @@ import org.apache.sis.util.iso.Names;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.WeakValueHashMap;
import org.apache.sis.feature.privy.FeatureExpression;
+import org.apache.sis.feature.privy.FeatureProjectionBuilder;
import org.apache.sis.filter.internal.Node;
-import org.apache.sis.feature.builder.FeatureTypeBuilder;
-import org.apache.sis.feature.builder.PropertyTypeBuilder;
import org.apache.sis.math.FunctionProperty;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.FeatureType;
import org.opengis.feature.AttributeType;
import org.opengis.filter.Expression;
@@ -151,14 +149,15 @@ abstract class LeafExpression<R,V> extends Node implements FeatureExpression<R,V
}
/**
- * Provides the type of values returned by {@link #apply(Object)}
- * wrapped in an {@link AttributeType} named "Literal".
+ * Provides the type of values returned by {@link #apply(Object)}.
+ * The returned item wraps an {@link AttributeType} named "Literal".
+ * The attribute type is determined by the class of the {@linkplain #value}.
*
* @param addTo where to add the type of properties evaluated by the given expression.
- * @return builder of the added property.
+ * @return handler for the added property.
*/
@Override
- public PropertyTypeBuilder expectedType(FeatureType ignored, final FeatureTypeBuilder addTo) {
+ public FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
final Class<?> valueType = getValueClass();
AttributeType<?> propertyType;
synchronized (TYPES) {
@@ -171,7 +170,7 @@ abstract class LeafExpression<R,V> extends Node implements FeatureExpression<R,V
}
}
}
- return addTo.addProperty(propertyType);
+ return addTo.addSourceProperty(propertyType, true);
}
/**
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
index bb62d7559c..473b98d4db 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
@@ -23,9 +23,7 @@ import org.apache.sis.feature.Features;
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.feature.privy.FeatureProjectionBuilder;
import org.apache.sis.filter.privy.XPath;
import org.apache.sis.util.resources.Errors;
@@ -34,8 +32,6 @@ import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
import org.opengis.feature.PropertyType;
import org.opengis.feature.AttributeType;
-import org.opengis.feature.IdentifiedType;
-import org.opengis.feature.Operation;
import org.opengis.feature.PropertyNotFoundException;
import org.opengis.filter.ValueReference;
@@ -68,8 +64,8 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V>
/**
* 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.
+ * If {@code true}, then {@link #expectedType(FeatureProjectionBuilder)} will not throw an exception when
+ * the property is not found.
*/
protected final boolean isVirtual;
@@ -172,11 +168,15 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V>
}
/**
- * Returns the default value of {@link #expectedType(FeatureType, FeatureTypeBuilder)}
- * when it cannot be inferred by the analysis of the given {@code FeatureType}.
+ * Returns the value of {@code expectedType(…)} to return as a fallback when
+ * that value cannot be inferred by the analysis of the source feature type.
+ * It happens when {@link #isVirtual} is {@code true} and the property to add may be
+ * defined in a feature sub-type not known at {@code expectedType(…)} invocation time.
+ *
+ * @see #expectedType(FeatureProjectionBuilder)
*/
- final PropertyTypeBuilder expectedType(final FeatureTypeBuilder addTo) {
- return addTo.addAttribute(getValueClass()).setName(name).setMinimumOccurs(0);
+ final FeatureProjectionBuilder.Item defaultType(final FeatureProjectionBuilder addTo) {
+ return addTo.addOptionalAttribute(name, getValueClass());
}
/**
@@ -349,53 +349,48 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V>
}
/**
- * Provides the expected type of values produced by this expression
- * when a feature of the given type is evaluated.
+ * Provides the expected type of values produced by this expression.
+ * If the converted {@linkplain #type} is a generalization of the attribute type,
+ * the original attribute type is kept unchanged because {@link #apply(Feature)}
+ * does not convert those values.
+ *
+ * @throws UnconvertibleObjectException if the property default value cannot be converted to {@link #type}.
*/
@Override
- public final PropertyTypeBuilder expectedType(final FeatureType valueType, final FeatureTypeBuilder addTo) {
- final PropertyTypeBuilder p = super.expectedType(valueType, addTo);
- if (p instanceof AttributeTypeBuilder<?>) {
- final AttributeTypeBuilder<?> a = (AttributeTypeBuilder<?>) p;
- if (!type.isAssignableFrom(a.getValueClass())) {
- return a.setValueClass(type);
- }
- }
- return p;
+ public final FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
+ FeatureProjectionBuilder.Item item = super.expectedType(addTo);
+ item.replaceValueClass((c) -> type.isAssignableFrom(c) ? c : type);
+ return item;
}
}
/**
* Provides the expected type of values produced by this expression when a feature of the given type is evaluated.
+ * The source feature type is specified indirectly by {@link FeatureProjectionBuilder#source()}.
*
- * @param valueType the type of features to be evaluated by the given expression.
- * @param addTo where to add the type of properties evaluated by the given expression.
+ * <h4>Handling of operations</h4>
+ * Properties that are operations are replaced by attributes where the operation result will be stored.
+ * An exception to this rule is the links such as {@code "sis:identifier"} and {@code "sis:geometry"},
+ * in which case the link operation is kept. It may force {@code FeatureProjectionBuilder} to add also
+ * the dependencies (targets) of the link.
+ *
+ * @param addTo where to add the type of properties evaluated by this expression.
* @return builder of the added property, or {@code null} if this method cannot add a property.
- * @throws IllegalArgumentException if this method cannot determine the property type for the given feature type.
+ * @throws PropertyNotFoundException if the property was not found in {@code addTo.source()}.
*/
@Override
- public PropertyTypeBuilder expectedType(final FeatureType valueType, final FeatureTypeBuilder addTo) {
+ public FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
PropertyType type;
try {
- type = valueType.getProperty(name);
+ type = addTo.source().getProperty(name);
} catch (PropertyNotFoundException e) {
if (isVirtual) {
- // The property does not exist but may be defined on a yet unknown child type.
- return expectedType(addTo);
+ // The property does not exist but may be defined in a yet unknown child type.
+ return defaultType(addTo);
}
throw e;
}
- while (type instanceof Operation) {
- final IdentifiedType result = ((Operation) type).getResult();
- if (result != type && result instanceof PropertyType) {
- type = (PropertyType) result;
- } else if (result instanceof FeatureType) {
- return addTo.addAssociation((FeatureType) result).setName(name);
- } else {
- return null;
- }
- }
- return addTo.addProperty(type);
+ return addTo.addSourceProperty(type, true);
}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/XPath.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/XPath.java
index 30930dc451..679b7631c8 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/XPath.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/XPath.java
@@ -23,6 +23,7 @@ import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.iso.DefaultNameSpace;
import static org.apache.sis.util.CharSequences.*;
import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.filter.InvalidXPathException;
/**
@@ -214,6 +215,7 @@ public final class XPath {
/**
* Rewrites a property name as an XPath.
* The path components are assumed already parsed (no braced URI literals).
+ * If the tip contain path separator ({@code '/'}), the path will be escaped in a {@code Q{…}} form.
*
* @param prefix a prefix, or {@code null} if none.
* @param path components of the XPath before the tip, or {@code null} if none.
@@ -233,19 +235,31 @@ public final class XPath {
}
}
+ /**
+ * Returns a XPath for the given property name. In the usual case where the given name
+ * does not use reserved characters, that name is returned as-is.
+ *
+ * @param name the property name.
+ * @return the given name as an XPath.
+ */
+ public static String fromPropertyName(final String name) {
+ // TODO: do we need to escape '{' and '}'?
+ return toString(null, null, name);
+ }
+
/**
* Rewrites the XPath in a form accepted by feature properties.
* This is the tip, without {@code "Q{namespace}"} escaping.
*
* @param xpath the XPath to convert to a property name.
* @return the XPath as a property name without escape syntax for qualified URIs.
- * @throws IllegalArgumentException if the given XPath contains a path instead of only a tip.
+ * @throws InvalidXPathException if the given XPath contains a path instead of only a tip.
*/
public static String toPropertyName(final String xpath) {
final var x = new XPath(xpath);
if (x.path == null) {
return x.tip;
}
- throw new IllegalArgumentException(Resources.format(Resources.Keys.PropertyNameCannotBeXPath_1, xpath));
+ throw new InvalidXPathException(Resources.format(Resources.Keys.PropertyNameCannotBeXPath_1, xpath));
}
}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
index 3e56cef2da..2d751dd31e 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
@@ -24,14 +24,11 @@ import org.opengis.util.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.factory.InvalidGeodeticParameterException;
-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.feature.privy.FeatureProjectionBuilder;
import org.apache.sis.util.privy.Constants;
import org.apache.sis.util.resources.Errors;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.FeatureType;
import org.opengis.filter.Literal;
import org.opengis.filter.Expression;
import org.opengis.filter.InvalidFilterValueException;
@@ -213,20 +210,15 @@ search: if (crs instanceof CoordinateReferenceSystem) {
}
/**
- * Provides the type of values produced by this expression when a feature of the given type is evaluated.
+ * Provides the type of values produced by this expression.
+ * This is the value computed by the parent class except for the <abbr>SRID</abbr>.
*
- * @param valueType the type of features on which to apply this expression.
- * @param addTo where to add the type of properties evaluated by this expression.
- * @return builder of type resulting from expression evaluation (never null).
- * @throws IllegalArgumentException if the given feature type does not contain the expected properties.
+ * @param addTo where to add the type of properties evaluated by this expression.
+ * @return handler of type resulting from expression evaluation (never null).
*/
@Override
- public PropertyTypeBuilder expectedType(final FeatureType valueType, final FeatureTypeBuilder addTo) {
- final PropertyTypeBuilder pt = super.expectedType(valueType, addTo);
- if (pt instanceof AttributeTypeBuilder<?>) {
- // We must unconditionally override the CRS set by parent class.
- ((AttributeTypeBuilder<?>) pt).setCRS(literalCRS ? targetCRS : null);
- }
- return pt;
+ public FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
+ // We must unconditionally overwrite the CRS set by the parent class.
+ return super.expectedType(addTo).setCRS(literalCRS ? targetCRS : null);
}
}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
index c127135a4b..92e5900a57 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
@@ -24,16 +24,13 @@ import org.apache.sis.filter.Optimization;
import org.apache.sis.filter.internal.Node;
import org.apache.sis.feature.internal.Resources;
import org.apache.sis.feature.privy.FeatureExpression;
+import org.apache.sis.feature.privy.FeatureProjectionBuilder;
import org.apache.sis.geometry.wrapper.Geometries;
-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.ArgumentChecks;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.iso.Names;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.FeatureType;
import org.opengis.filter.Expression;
import org.opengis.filter.InvalidFilterValueException;
@@ -171,7 +168,7 @@ abstract class SpatialFunction<R> extends Node implements FeatureExpression<R,Ob
}
/**
- * Provides the type of values produced by this expression when a feature of the given type is evaluated.
+ * Provides the type of values produced by this expression.
* There are two cases:
*
* <ul class="verbose">
@@ -181,33 +178,28 @@ abstract class SpatialFunction<R> extends Node implements FeatureExpression<R,Ob
* <li>Otherwise an attribute is created with the return value specified by the operation.</li>
* </ul>
*
- * @param valueType the type of features on which to apply this expression.
- * @param addTo where to add the type of properties evaluated by this expression.
+ * @param addTo where to add the type of properties evaluated by this expression.
* @return builder of type resulting from expression evaluation (never null).
- * @throws InvalidFilterValueException if the given feature type does not contain the expected properties,
+ * @throws InvalidFilterValueException if the source feature type does not contain the expected properties,
* or if this method cannot determine the result type of the expression.
* It may be because that expression is backed by an unsupported implementation.
*/
@Override
- public PropertyTypeBuilder expectedType(final FeatureType valueType, final FeatureTypeBuilder addTo) {
- AttributeTypeBuilder<?> att;
-cases: if (operation.isGeometryInOut()) {
+ public FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
+ if (operation.isGeometryInOut()) {
final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(getParameters().get(0));
if (fex != null) {
- final PropertyTypeBuilder type = fex.expectedType(valueType, addTo);
- if (type instanceof AttributeTypeBuilder<?>) {
- att = (AttributeTypeBuilder<?>) type;
- final Geometries<?> library = Geometries.factory(att.getValueClass());
- if (library != null) {
- att = att.setValueClass(operation.getReturnType(library));
- break cases;
- }
+ final FeatureProjectionBuilder.Item item = fex.expectedType(addTo);
+ final boolean success = item.replaceValueClass((c) -> {
+ final Geometries<?> library = Geometries.factory(c);
+ return (library == null) ? null : operation.getReturnType(library);
+ });
+ if (success) {
+ return item;
}
}
throw new InvalidFilterValueException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression));
- } else {
- att = addTo.addAttribute(getValueClass());
}
- return att.setName(getFunctionName());
+ return addTo.addComputedAttribute(getFunctionName(), getValueClass());
}
}
diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
index 428b947eac..4636952051 100644
--- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
@@ -25,7 +25,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.sis.metadata.sql.privy.SQLBuilder;
-import org.apache.sis.storage.base.FeatureProjection;
+import org.apache.sis.feature.privy.FeatureProjection;
import org.apache.sis.util.collection.WeakValueHashMap;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -107,21 +107,9 @@ final class FeatureIterator implements Spliterator<Feature>, AutoCloseable {
* Additional properties to compute from the main properties, or {@code null} if none.
* This is usually {@code null}. It may be non-null if the user specified a query with
* operations other than {@code ValueReference} or {@code Literal}.
- *
- * <h4>Completion</h4>
- * Usually, the expressions are executed on a source feature instance and the results
- * are copied in a target feature instance. However, if {@link #completion} is true,
- * then the source and target features are the same instance. The completion mode is
- * used when the target feature have all information needed for computing the query
- * expressions, so that the projection is only a completion.
*/
private final FeatureProjection projection;
- /**
- * Whether the {@linkplain #projection} should be applied on the same
- */
- private final boolean completion;
-
/**
* Creates a new iterator over features.
*
@@ -130,8 +118,8 @@ final class FeatureIterator implements Spliterator<Feature>, AutoCloseable {
* @param distinct whether the set should contain distinct feature instances.
* @param selection condition to append, not including the {@code WHERE} keyword.
* @param sort the {@code ORDER BY} clauses, or {@code null} if none.
- * @param offset number of rows to skip in underlying SQL query, or ≤ 0 for none.
- * @param count maximum number of rows to return, or ≤ 0 for no limit.
+ * @param offset number of rows to skip in underlying SQL query, or 0 for none.
+ * @param count maximum number of rows to return, or 0 (not -1) for no limit.
* @param projection additional properties to compute, or {@code null} if none.
*/
FeatureIterator(final Table table,
@@ -153,7 +141,7 @@ final class FeatureIterator implements Spliterator<Feature>, AutoCloseable {
spatialInformation = null;
}
final String filter = (selection != null) ? selection.query(connection, spatialInformation) : null;
- if (distinct || filter != null || sort != null || offset > 0 || count > 0) {
+ if (distinct || filter != null || sort != null || (offset | count) != 0) {
final var builder = new SQLBuilder(table.database).append(sql);
if (distinct) {
builder.insertDistinctAfterSelect();
@@ -180,13 +168,13 @@ final class FeatureIterator implements Spliterator<Feature>, AutoCloseable {
* for metadata (probably a bug, but not all JDBC drivers are mature).
*/
if (filter == null) {
- estimatedSize = Math.min(table.countRows(connection.getMetaData(), distinct, true), offset + count) - offset;
+ final long n = table.countRows(connection.getMetaData(), distinct, true) - offset;
+ estimatedSize = (count != 0) ? Math.min(n, count) : n;
} else {
estimatedSize = 0; // Cannot estimate the size if there is filtering conditions.
}
this.result = connection.createStatement().executeQuery(sql);
this.dependencies = new FeatureIterator[adapter.dependencies.length];
- this.completion = projection != null && projection.featureType == table.featureType;
this.projection = projection;
this.statement = null;
}
@@ -209,7 +197,6 @@ final class FeatureIterator implements Spliterator<Feature>, AutoCloseable {
this.adapter = adapter;
statement = connection.prepareStatement(adapter.sql);
dependencies = new FeatureIterator[adapter.dependencies.length];
- completion = false;
projection = null;
estimatedSize = 0;
}
@@ -327,11 +314,7 @@ final class FeatureIterator implements Spliterator<Feature>, AutoCloseable {
* complex expressions, we need to handle them in Java code.
*/
if (projection != null) {
- if (completion) {
- projection.applySelf(feature);
- } else {
- feature = projection.apply(feature);
- }
+ feature = projection.apply(feature);
}
action.accept(feature);
if (!all) return true;
diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
index d2a5b14106..2386a8fa35 100644
--- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
@@ -16,9 +16,8 @@
*/
package org.apache.sis.storage.sql.feature;
-import java.util.Set;
+import java.util.BitSet;
import java.util.HashSet;
-import java.util.LinkedHashMap;
import java.util.Objects;
import java.util.Comparator;
import java.util.Spliterator;
@@ -32,7 +31,6 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import org.apache.sis.filter.Optimization;
-import org.apache.sis.filter.privy.ListingPropertyVisitor;
import org.apache.sis.filter.privy.SortByComparator;
import org.apache.sis.filter.privy.WarningEvent;
import org.apache.sis.metadata.sql.privy.SQLBuilder;
@@ -41,11 +39,10 @@ import org.apache.sis.util.privy.Strings;
import org.apache.sis.util.stream.DeferredStream;
import org.apache.sis.util.stream.PaginedStream;
import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.base.FeatureProjection;
+import org.apache.sis.feature.privy.FeatureProjection;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
-import org.opengis.filter.Expression;
import org.opengis.filter.Filter;
import org.opengis.filter.SortBy;
@@ -122,8 +119,10 @@ final class FeatureStream extends DeferredStream<Feature> {
private long offset;
/**
- * Maximum number of rows to return, or 0 for no limit. Note that 0 is a valid value for the limit,
- * but when this value is reached the {@link #empty()} stream should be immediately returned.
+ * Maximum number of rows to return, or 0 for no limit. Note: the use of 0 for "no limit" is unusual
+ * (a more usual value is -1) because 0 could be valid value for the limit. However, the methods in
+ * this class should return {@link #empty()} eagerly when they detect that the stream will be empty.
+ * Therefore, we should not have instances of {@code FeatureStream} with a limit which is really zero.
*
* @see #limit(long)
*/
@@ -425,28 +424,17 @@ final class FeatureStream extends DeferredStream<Feature> {
Table projected = table;
FeatureProjection completion = null;
if (projection != null) {
- final var unhandled = new LinkedHashMap<String, Expression<? super Feature, ?>>();
+ final var unhandled = new BitSet();
final var reusedNames = new HashSet<String>();
projected = new Table(projected, projection, reusedNames, unhandled);
- if (!unhandled.isEmpty()) {
+ completion = projection.afterPreprocessing(unhandled.stream().toArray());
+ if (completion != null && !reusedNames.containsAll(completion.dependencies())) {
/*
- * Some properties may not be handled by the projection. Check if the projected feature contains
- * enough information for computing missing properties without the need for the original feature.
+ * Cannot use `projected` because some expressions need properties available only
+ * in the source features. Request full feature instances from the original table.
*/
- Set<String> references = null;
- for (var expression : unhandled.values()) {
- references = ListingPropertyVisitor.xpaths(expression, references);
- }
- if (references == null || reusedNames.containsAll(references)) {
- completion = new FeatureProjection(projected.featureType, unhandled);
- } else {
- /*
- * Cannot use `projected` because some expressions need properties
- * available only in the original features.
- */
- projected = table;
- completion = projection;
- }
+ projected = table;
+ completion = projection;
}
}
lock(projected.database.transactionLocks);
diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
index 65dc579abb..1a56e7149e 100644
--- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
@@ -19,6 +19,7 @@ package org.apache.sis.storage.sql.feature;
import java.util.Map;
import java.util.Set;
import java.util.List;
+import java.util.BitSet;
import java.util.Optional;
import java.util.stream.Stream;
import java.sql.Connection;
@@ -28,10 +29,12 @@ import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import org.opengis.util.GenericName;
import org.opengis.geometry.Envelope;
+import org.apache.sis.filter.privy.XPath;
+import org.apache.sis.filter.InvalidXPathException;
import org.apache.sis.storage.AbstractFeatureSet;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.InternalDataStoreException;
-import org.apache.sis.storage.base.FeatureProjection;
+import org.apache.sis.feature.privy.FeatureProjection;
import org.apache.sis.metadata.sql.privy.Reflection;
import org.apache.sis.metadata.sql.privy.SQLBuilder;
import org.apache.sis.pending.jdk.JDK19;
@@ -48,7 +51,6 @@ import org.opengis.feature.Feature;
import org.opengis.feature.FeatureType;
import org.opengis.feature.AttributeType;
import org.opengis.feature.FeatureAssociationRole;
-import org.opengis.filter.Expression;
import org.opengis.filter.InvalidFilterValueException;
@@ -78,6 +80,13 @@ final class Table extends AbstractFeatureSet {
* except synthetic attributes like "sis:identifier". The feature may also contain associations
* inferred from foreigner keys that are not immediately apparent in the table.
*
+ * <h4>Relationship with {@code FeatureSet.getType()}</h4>
+ * When this {@code Table} has been created by inspection of the database metadata, this feature type
+ * is the value shown to users. But when this {@code Table} has been created from a query, this value
+ * is not directly shown to user because it may contain additional properties for the dependencies of
+ * operations. In the latter case, this table is hidden behind {@link FeatureIterator} and the type
+ * seen by user is provided by {@link org.apache.sis.storage.FeatureSubset#resultType}.
+ *
* @see #getType()
*/
final FeatureType featureType;
@@ -136,7 +145,7 @@ final class Table extends AbstractFeatureSet {
final boolean hasRaster;
/**
- * Map from attribute name to columns. This is built from {@link #columns} array when first needed.
+ * Map from attribute name to columns. This is built from the {@link #attributes} array when first needed.
*
* @see #getColumn(String)
*/
@@ -195,27 +204,27 @@ final class Table extends AbstractFeatureSet {
* Creates a new table as a projection (subset of columns) of the given table.
* The columns to retain, potentially under different names, are specified in {@code projection}.
* The projection may also contain complex expressions that cannot be handled by this constructor.
- * Such expressions are stored in the {@code unhandled} map.
+ * Such expressions have their indexes stored in the {@code unhandled} set.
*
- * @param table the source table.
+ * @param source the source table.
* @param projection description of the columns to keep.
* @param reusedNames an initially empty set where to store the names of attributes that are not renamed.
- * @param unhandled an initially empty map where to add expressions that are not handled by the new table.
+ * @param unhandled where to set the bits for indexes of expressions that are not handled by the new table.
* @throws InvalidFilterValueException if there is an error in the declaration of property values.
*/
@SuppressWarnings("LocalVariableHidesMemberVariable")
Table(final Table source,
final FeatureProjection projection,
final Set<String> reusedNames,
- final Map<String, Expression<? super Feature, ?>> unhandled)
+ final BitSet unhandled)
{
super(source.listeners, false);
database = source.database;
query = source.query;
name = source.name;
- featureType = projection.featureType;
+ featureType = projection.typeWithDependencies;
/*
- * Temporary values the fields, before assignment to final fields.
+ * Temporary values of fields, before assignment to final fields.
* The final number of attributes and foreigner keys may be smaller.
*/
final var attributes = new Column [source.attributes .length];
@@ -229,12 +238,12 @@ final class Table extends AbstractFeatureSet {
* Take all columns that we can put in the `WHERE` clause of the SQL statement.
* The columns that we cannot take will be declared in the `unhandled` bit set.
*/
- final List<String> storedProperties = projection.getStoredPropertyNames();
+ final List<String> storedProperties = projection.propertiesToCopy();
final int count = storedProperties.size();
for (int i=0; i<count; i++) {
- final String xpath = projection.getSourcePropertyName(i, false);
+ final String xpath = projection.xpath(i).orElse(null);
if (xpath == null) {
- unhandled.put(storedProperties.get(i), projection.getExpression(i));
+ unhandled.set(i);
continue;
}
final Column column = source.getColumn(xpath);
@@ -274,7 +283,7 @@ final class Table extends AbstractFeatureSet {
if (source.primaryKey != null) {
for (String column : source.primaryKey.getColumns()) {
final int i = storedProperties.indexOf(column);
- if (i < 0 || !column.equals(projection.getSourcePropertyName(i, true))) {
+ if (i < 0 || !column.equals(projection.xpath(i).orElse(null))) {
primaryKey = null;
return;
}
@@ -386,6 +395,8 @@ final class Table extends AbstractFeatureSet {
/**
* Returns the feature type inferred from the database structure analysis.
+ * Note that this type may not be the type shown to user if this table is
+ * created behind a {@link org.apache.sis.storage.FeatureSubset}.
*/
@Override
public final FeatureType getType() {
@@ -435,6 +446,7 @@ final class Table extends AbstractFeatureSet {
*
* @param xpath the XPath (currently only attribute name).
* @return column for the given XPath, or {@code null} if the specified attribute is not found.
+ * @throws InvalidXPathException if the given XPath is not supported.
*/
final Column getColumn(final String xpath) {
Map<String,Column> m;
@@ -453,18 +465,20 @@ final class Table extends AbstractFeatureSet {
attributeToColumns = m;
}
}
- return m.get(xpath);
+ return m.get(XPath.toPropertyName(xpath)); // XPaths other than tips are not yet supported.
}
/**
* Returns the relation from an attribute name specified as XPath.
- * See {@link #getColumn(String)} for a note on the wat that {@code xpath} is interpreted.
+ * See {@link #getColumn(String)} for a note on the way that {@code xpath} is interpreted.
*
* @param xpath the XPath (currently only attribute name).
* @param exports {@code true} for searching in exported keys, or {@code false} for searching in imported keys.
* @return relation for the given XPath, or {@code null} if the specified attribute is not found.
+ * @throws InvalidXPathException if the given XPath is not supported.
*/
- private Relation getRelation(final String xpath, final boolean exports) {
+ private Relation getRelation(String xpath, final boolean exports) {
+ xpath = XPath.toPropertyName(xpath); // XPaths other than tips are not yet supported.
boolean tip = false;
do { // Execute 1 or 2 times: check tips only if no exact match.
for (final Relation c : (exports ? exportedKeys : importedKeys)) {
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
index 7a961f9af1..80dec5de00 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
@@ -19,7 +19,9 @@ package org.apache.sis.storage;
import java.util.Arrays;
import java.util.Set;
import java.util.Map;
+import java.util.Locale;
import java.util.Objects;
+import java.util.Optional;
import java.util.OptionalLong;
import java.util.function.Function;
import java.io.Serializable;
@@ -29,6 +31,9 @@ import org.opengis.util.GenericName;
import org.opengis.geometry.Envelope;
import org.apache.sis.feature.FeatureOperations;
import org.apache.sis.feature.privy.AttributeConvention;
+import org.apache.sis.feature.privy.FeatureExpression;
+import org.apache.sis.feature.privy.FeatureProjection;
+import org.apache.sis.feature.privy.FeatureProjectionBuilder;
import org.apache.sis.filter.DefaultFilterFactory;
import org.apache.sis.filter.Optimization;
import org.apache.sis.filter.privy.ListingPropertyVisitor;
@@ -42,12 +47,14 @@ import org.apache.sis.util.iso.Names;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
import org.opengis.feature.Attribute;
import org.opengis.feature.AttributeType;
import org.opengis.feature.Operation;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.Filter;
import org.opengis.filter.Expression;
+import org.opengis.filter.InvalidFilterValueException;
import org.opengis.filter.Literal;
import org.opengis.filter.ValueReference;
import org.opengis.filter.SortBy;
@@ -389,6 +396,7 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali
@SafeVarargs
@SuppressWarnings("varargs")
public final void setSortBy(final SortProperty<Feature>... properties) {
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
SortBy<Feature> sortBy = null;
if (properties != null) {
sortBy = SortByComparator.create(properties);
@@ -527,7 +535,7 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali
*
* Columns can be given to the {@link FeatureQuery#setProjection(NamedExpression[])} method.
*
- * @version 1.4
+ * @version 1.5
* @since 1.1
*/
public static class NamedExpression implements Serializable {
@@ -606,6 +614,25 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali
this.alias = alias;
}
+ /**
+ * Adds this named expression as a property into the given builder.
+ *
+ * @param builder the builder where to add the property.
+ * @return whether the property has been successfully added.
+ */
+ final boolean addTo(final FeatureProjectionBuilder builder) {
+ final FeatureExpression<? super Feature, ?> fex = FeatureExpression.castOrCopy(expression);
+ if (fex != null) {
+ final FeatureProjectionBuilder.Item item = fex.expectedType(builder);
+ if (item != null) {
+ item.setName(alias); // Need to be invoked aven if the alias is null.
+ item.setValueGetter(expression, type == ProjectionType.STORED);
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Returns a hash code value for this column.
*
@@ -706,6 +733,36 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali
return xpaths;
}
+ /**
+ * Creates the projection (in <abbr>SQL</abbr> sense) of the given feature type.
+ * If some expressions have no name, default names are computed as below:
+ *
+ * <ul>
+ * <li>If the expression is an instance of {@link ValueReference}, the name of the
+ * property referenced by the {@linkplain ValueReference#getXPath() XPath}.</li>
+ * <li>Otherwise the localized string "Unnamed #1" with increasing numbers.</li>
+ * </ul>
+ *
+ * @param sourceType the feature type to project.
+ * @param locale locale for error messages, or {@code null} for the default locale.
+ */
+ final Optional<FeatureProjection> project(final FeatureType sourceType, final Locale locale) {
+ if (projection == null) {
+ return Optional.empty();
+ }
+ final var builder = new FeatureProjectionBuilder(sourceType, locale);
+ for (int column = 0; column < projection.length; column++) {
+ final NamedExpression item = projection[column];
+ if (!item.addTo(builder)) {
+ final var name = item.expression.getFunctionName().toInternationalString();
+ throw new InvalidFilterValueException(Resources.forLocale(locale)
+ .getString(Resources.Keys.InvalidExpression_2, column, name));
+ }
+ }
+ builder.setName(sourceType.getName());
+ return builder.project();
+ }
+
/**
* Applies this query on the given feature set.
* This method is invoked by the default implementation of {@link FeatureSet#subset(Query)}.
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
index 32159973bd..c4d4e792a7 100644
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
@@ -19,7 +19,7 @@ package org.apache.sis.storage;
import java.util.OptionalLong;
import java.util.stream.Stream;
import org.opengis.metadata.Metadata;
-import org.apache.sis.storage.base.FeatureProjection;
+import org.apache.sis.feature.privy.FeatureProjection;
import org.apache.sis.storage.base.MetadataBuilder;
import org.apache.sis.storage.base.StoreUtilities;
import org.apache.sis.storage.internal.Resources;
@@ -95,8 +95,8 @@ final class FeatureSubset extends AbstractFeatureSet {
if (resultType == null) {
final FeatureType type = source.getType();
try {
- projection = FeatureProjection.create(type, query.getProjection());
- resultType = (projection != null) ? projection.featureType : type;
+ projection = query.project(type, listeners.getLocale()).orElse(null);
+ resultType = (projection != null) ? projection.typeRequested : type;
} catch (IllegalArgumentException e) {
throw new DataStoreContentException(Resources.forLocale(listeners.getLocale())
.getString(Resources.Keys.CanNotDeriveTypeFromFeature_1, type.getName()), e);
diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/FeatureProjection.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/FeatureProjection.java
deleted file mode 100644
index 5a25571e28..0000000000
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/FeatureProjection.java
+++ /dev/null
@@ -1,383 +0,0 @@
-/*
- * 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.storage.base;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.ConcurrentModificationException;
-import java.util.function.UnaryOperator;
-import org.opengis.util.GenericName;
-import org.apache.sis.feature.AbstractOperation;
-import org.apache.sis.feature.FeatureOperations;
-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.feature.privy.FeatureExpression;
-import org.apache.sis.filter.privy.XPath;
-import org.apache.sis.storage.FeatureQuery;
-import org.apache.sis.storage.internal.Resources;
-import org.apache.sis.pending.jdk.JDK19;
-import org.apache.sis.util.ArraysExt;
-import org.apache.sis.util.iso.Names;
-import org.apache.sis.util.logging.Logging;
-import org.apache.sis.util.privy.UnmodifiableArrayList;
-import org.apache.sis.util.resources.Vocabulary;
-
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyNotFoundException;
-import org.opengis.filter.InvalidFilterValueException;
-import org.opengis.filter.Expression;
-import org.opengis.filter.ValueReference;
-
-
-/**
- * A function applying projections (with "projected" in the <abbr>SQL</abbr> database sense) of features.
- * Given full feature instances in input, this method returns feature instances containing only a subset
- * of the properties. The property values may also be different if they are computed by the expressions.
- *
- * @author Guilhem Legal (Geomatys)
- * @author Martin Desruisseaux (Geomatys)
- */
-public class FeatureProjection implements UnaryOperator<Feature> {
- /**
- * The type of features created by this mapper.
- */
- public final FeatureType featureType;
-
- /**
- * Names of the properties to be stored in the target features. This is inferred from the {@code properties}
- * given at construction time, retaining only the elements of type {@link FeatureQuery.ProjectionType#STORED}.
- * Properties that are computed on-the-fly from other properties are not included in this list.
- * This array is arbitrarily set to {@code null} if this projection is an identity operation.
- *
- * <p>When a new {@code Feature} instance needs to be created with a subset of the properties
- * of the original feature instance, usually only the stored properties need to be copied.
- * Other properties computed on-the-fly from stored properties may not need copy.</p>
- *
- * @see #isIdentity()
- */
- private final String[] storedProperties;
-
- /**
- * Expressions to apply on the source feature for fetching the property values of the projected feature.
- * Shall have the same length as {@link #storedProperties} and the properties shall be in the same order.
- * This array is arbitrarily set to {@code null} if this projection is an identity operation.
- *
- * @see #isIdentity()
- */
- private final Expression<? super Feature, ?>[] expressions;
-
- /**
- * Infers the type of values evaluated by a query when executed on features of the given type.
- * If some expressions have no name, default names are computed as below:
- *
- * <ul>
- * <li>If the expression is an instance of {@link ValueReference}, the name of the
- * property referenced by the {@linkplain ValueReference#getXPath() XPath}.</li>
- * <li>Otherwise the localized string "Unnamed #1" with increasing numbers.</li>
- * </ul>
- *
- * <h4>Identity operation</h4>
- * If the result is a feature type with all the properties of the source feature,
- * with the same property names in the same order, and if the expressions are only
- * fetching the values (no computation), then this method returns {@code null} for
- * meaning that this projection does nothing.
- *
- * @param sourceType the type of features to be converted to projected features.
- * @param projection descriptions of the properties to keep in the projected features, or {@code null} if none.
- * @return a function for projecting the feature instances, or {@code null} if none.
- * @throws IllegalArgumentException if this method can operate only on some feature types
- * and the given type is not one of them.
- * @throws InvalidFilterValueException if this method cannot determine the result type of an expression.
- * It may be because that expression is backed by an unsupported implementation.
- */
- public static FeatureProjection create(final FeatureType sourceType, final FeatureQuery.NamedExpression[] projection) {
- if (projection != null) {
- var fp = new FeatureProjection(sourceType, projection);
- if (!fp.isIdentity()) {
- return fp;
- }
- }
- return null;
- }
-
- /**
- * Creates a new feature projection. If some expressions have no name,
- * default names are computed as described in {@link #create create(…)}.
- *
- * <p>Callers shall invoke {@link #isIdentity()} after construction.
- * This instance shall not be used if it is the identity operation.</p>
- *
- * @param sourceType the type of features to be converted to projected features.
- * @param projection descriptions of the properties to keep in the projected features, or {@code null} if none.
- * @throws IllegalArgumentException if this method can operate only on some feature types
- * and the given type is not one of them.
- * @throws InvalidFilterValueException if this method cannot determine the result type of an expression.
- * It may be because that expression is backed by an unsupported implementation.
- */
- protected FeatureProjection(final FeatureType sourceType, final FeatureQuery.NamedExpression[] projection) {
- int storedCount = 0;
- int unnamedNumber = 0; // Sequential number for unnamed expressions.
- Set<String> names = null; // Names already used, for avoiding collisions.
-
- // For detecting if the projection would be an identity operation.
- var propertiesOfIdentity = sourceType.getProperties(true).iterator();
-
- @SuppressWarnings({"LocalVariableHidesMemberVariable", "unchecked", "rawtypes"})
- final Expression<? super Feature,?>[] expressions = new Expression[projection.length];
-
- @SuppressWarnings("LocalVariableHidesMemberVariable")
- final String[] storedProperties = new String[projection.length];
-
- final FeatureTypeBuilder ftb = new FeatureTypeBuilder().setName(sourceType.getName());
- for (int column = 0; column < projection.length; column++) {
- final FeatureQuery.NamedExpression item = projection[column];
- /*
- * For each property, get the expected type (mandatory) and its name (optional).
- * A default name will be computed if no alias was explicitly given by the user.
- */
- final var expression = item.expression;
- final var fex = FeatureExpression.castOrCopy(expression);
- final PropertyTypeBuilder resultType;
- if (fex == null || (resultType = fex.expectedType(sourceType, ftb)) == null) {
- throw new InvalidFilterValueException(Resources.format(Resources.Keys.InvalidExpression_2,
- expression.getFunctionName().toInternationalString(), column));
- }
- GenericName name = item.alias;
- if (name == null) {
- /*
- * Build a list of aliases declared by the user, for making sure that we do not collide with them.
- * No check for `GenericName` collision here because it was already verified by `setProjection(…)`.
- * We may have collision of their `String` representations however, which is okay.
- */
- if (names == null) {
- names = JDK19.newHashSet(projection.length);
- for (final FeatureQuery.NamedExpression p : projection) {
- if (p.alias != null) {
- names.add(p.alias.toString());
- }
- }
- }
- /*
- * If the expression is a `ValueReference`, the `PropertyType` instance can be taken directly
- * from the source feature (the Apache SIS implementation does just that). If the name is set,
- * then we assume that it is correct. Otherwise we take the tip of the XPath.
- */
- String tip = reference(expression, true);
- if (tip != null) {
- /*
- * Take the existing `GenericName` instance from the property. It should be equivalent to
- * creating a name from the tip, except that it may be a `ScopedName` instead of `LocalName`.
- * We do not take `resultType.getName()` because the latter is different if the property
- * is itself a link to another property (in which case `resultType` is the final target).
- */
- name = sourceType.getProperty(tip).getName();
- if (name == null || !names.add(name.toString())) {
- name = null;
- if (tip.isBlank() || names.contains(tip)) {
- tip = null;
- }
- }
- }
- /*
- * If we still have no name at this point, create a name like "Unnamed #1".
- * Note that despite the use of `Vocabulary` resources, the name will be unlocalized
- * (for easier programmatic use) because `GenericName` implementation is designed for
- * providing localized names only if explicitly requested.
- */
- if (name == null) {
- CharSequence text = tip;
- if (text == null) do {
- text = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed_1, ++unnamedNumber);
- } while (!names.add(text.toString()));
- name = Names.createLocalName(null, null, text);
- }
- }
- /*
- * If the attribute that we just added should be virtual, replace the attribute by an operation.
- * When `FeatureProjection` contains at least one such virtual attributes, the result cannot be
- * an identity operation.
- */
- if (item.type != FeatureQuery.ProjectionType.STORED) {
- propertiesOfIdentity = null; // No longer an identity operation.
- if (resultType instanceof AttributeTypeBuilder<?>) {
- final var ab = (AttributeTypeBuilder<?>) resultType;
- final var storedType = ab.build();
- if (ftb.properties().remove(resultType)) {
- final var identification = Map.of(AbstractOperation.NAME_KEY, name);
- ftb.addProperty(FeatureOperations.expression(identification, expression, storedType));
- }
- }
- continue;
- }
- /*
- * This is the usual case where the property value is copied in the "projected" feature.
- * If the target property name is equal to the source property name, in the same order,
- * and the expression is just fetching the value from a property of the same name, then
- * we consider that the projection is an identity operation.
- */
- resultType.setName(name);
- storedProperties[storedCount] = name.toString();
- expressions[storedCount++] = expression;
-isIdentity: if (propertiesOfIdentity != null) {
- if (propertiesOfIdentity.hasNext()) {
- final var property = propertiesOfIdentity.next();
- if (name.equals(property.getName())) {
- final String tip = name.tip().toString();
- if (tip.equals(reference(expression, true))) try {
- if (property.equals(sourceType.getProperty(tip))) {
- break isIdentity; // Continue to consider that we may have an identity projection.
- }
- } catch (PropertyNotFoundException e) {
- // It may be because the property name is ambiguous.
- Logging.ignorableException(StoreUtilities.LOGGER, FeatureProjection.class, "create", e);
- }
- }
- }
- propertiesOfIdentity = null;
- }
- }
- /*
- * End of the construction of properties from the given expressions. If all properties
- * are just copying values of properties of the same name from the source, returns `null`.
- */
- if (propertiesOfIdentity == null || propertiesOfIdentity.hasNext()) {
- this.featureType = ftb.build();
- this.storedProperties = ArraysExt.resize(storedProperties, storedCount);
- this.expressions = ArraysExt.resize(expressions, storedCount);
- } else {
- this.featureType = sourceType;
- this.storedProperties = null;
- this.expressions = null; // Means "identity operation".
- }
- }
-
- /**
- * Creates a new projection with the given properties.
- *
- * @param type the type of feature instances created by this projection.
- * @param projection all properties by name, associated to expressions for getting values from source features.
- */
- @SuppressWarnings({"rawtypes", "unchecked"})
- public FeatureProjection(final FeatureType type, final Map<String, Expression<? super Feature, ?>> projection) {
- featureType = type;
- expressions = new Expression[projection.size()];
- storedProperties = new String[expressions.length];
- int i = 0;
- for (var entry : projection.entrySet()) {
- storedProperties[i] = entry.getKey();
- expressions[i++] = entry.getValue();
- }
- if (i != expressions.length) {
- // Should never happen, unless the `Map` has been modified concurrently.
- throw new ConcurrentModificationException();
- }
- }
-
- /**
- * If the given expression is a value reference, returns the tip of the referenced property.
- * Otherwise, returns {@code null}.
- *
- * @param expression the expression from which to get the referenced property name.
- * @param decode whether to decode the XPath syntax (e.g. {@code {@code "Q{namespace}"}} syntax).
- * @return the referenced property name, or {@code null} if none.
- */
- private static String reference(final Expression<? super Feature, ?> expression, final boolean decode) {
- if (expression instanceof ValueReference<?,?>) {
- String xpath = ((ValueReference<?,?>) expression).getXPath();
- return decode ? XPath.toPropertyName(xpath) : xpath;
- }
- return null;
- }
-
- /**
- * Returns whether this projection performs no operation.
- * If this method returns {@code true}, then this instance shall <strong>not</strong> be used.
- *
- * @return whether this projection performs no operation.
- */
- public final boolean isIdentity() {
- return expressions == null;
- }
-
- /**
- * Returns the names of all stored properties. This list may be shorter than the list of properties
- * of the {@linkplain #featureType feature type} if some feature properties are computed on-the-fly.
- *
- * @return the name of all stored properties.
- */
- public final List<String> getStoredPropertyNames() {
- return UnmodifiableArrayList.wrap(storedProperties);
- }
-
- /**
- * Returns the name of a property in the source features.
- * The given index corresponds to an index in the list returned by {@link #getStoredPropertyNames()}.
- * The return value is often the same name as {@code getStoredPropertyNames().get(index)}.
- *
- * @param index index of the stored property for which to get the name in the source feature.
- * @param decode whether to decode the XPath syntax (e.g. {@code {@code "Q{namespace}"}} syntax).
- * @return name in the source features, or {@code null} if the property is not a {@link ValueReference}.
- * @throws NullPointerException if {@link #isIdentity()} is {@code true}.
- */
- public final String getSourcePropertyName(final int index, final boolean decode) {
- return reference(expressions[index], decode);
- }
-
- /**
- * Returns the expression at the given index.
- *
- * @param index index of the stored property for which to get the expression.
- * @return expression at the given index.
- * @throws NullPointerException if {@link #isIdentity()} is {@code true}.
- */
- public final Expression<? super Feature, ?> getExpression(final int index) {
- return expressions[index];
- }
-
- /**
- * Derives a new projected feature instance from the given source.
- *
- * @param source the source feature instance.
- * @return the "projected" (<abbr>SQL</abbr> database sense) feature instance.
- * @throws NullPointerException if {@link #isIdentity()} is {@code true}.
- */
- @Override
- public final Feature apply(final Feature source) {
- final var feature = featureType.newInstance();
- for (int i=0; i < expressions.length; i++) {
- feature.setPropertyValue(storedProperties[i], expressions[i].apply(source));
- }
- return feature;
- }
-
- /**
- * Applies the expressions with the given feature as both the source and the target.
- * IT can be useful when all expressions are computing values derived from other attributes.
- *
- * @param feature the feature on which to apply the expressions.
- * @throws NullPointerException if {@link #isIdentity()} is {@code true}.
- */
- public final void applySelf(final Feature feature) {
- for (int i=0; i < expressions.length; i++) {
- feature.setPropertyValue(storedProperties[i], expressions[i].apply(feature));
- }
- }
-}