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
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new e821e9247b Add `FeatureProjectionTest` and fix bugs identified by the
tests:
e821e9247b is described below
commit e821e9247b8c4f76edd6e33c04ea894b15ef93bf
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Nov 19 19:37:53 2025 +0100
Add `FeatureProjectionTest` and fix bugs identified by the tests:
* ClassCastException when the result of an expression does not match the
type expected by the feature property.
* Inconsistency between the property type declared by `requestedType` and
the property type actually returned.
* Documentation and renaming for clarifying expectations and relationship
between fields and some methods.
* Consolidation of management of dependencies of expressions used in
projections (still work in progress).
---
.../apache/sis/feature/AbstractAssociation.java | 2 +-
.../org/apache/sis/feature/AbstractFeature.java | 2 +-
.../org/apache/sis/feature/AbstractOperation.java | 7 +-
.../org/apache/sis/feature/DefaultFeatureType.java | 2 +-
.../sis/feature/builder/FeatureTypeBuilder.java | 2 +
.../org/apache/sis/feature/internal/Resources.java | 6 +-
.../sis/feature/internal/Resources.properties | 2 +-
.../sis/feature/internal/Resources_fr.properties | 2 +-
.../feature/internal/shared/FeatureProjection.java | 46 ++-
.../internal/shared/FeatureProjectionBuilder.java | 416 +++++++++++----------
.../sis/feature/internal/shared/OperationView.java | 192 ++++++++++
.../main/org/apache/sis/filter/PropertyValue.java | 81 +++-
.../apache/sis/filter/base/ConvertFunction.java | 4 +-
.../main/org/apache/sis/filter/math/Function.java | 2 +
.../apache/sis/filter/sqlmm/FunctionWithSRID.java | 8 +-
.../apache/sis/filter/sqlmm/SpatialFunction.java | 8 +-
.../internal/shared/FeatureProjectionTest.java | 294 +++++++++++++++
.../sis/storage/sql/feature/FeatureAdapter.java | 2 +-
.../sis/storage/sql/feature/FeatureStream.java | 6 +-
.../main/org/apache/sis/storage/FeatureQuery.java | 2 +-
.../org/apache/sis/util/resources/Vocabulary.java | 5 +
.../sis/util/resources/Vocabulary.properties | 1 +
.../sis/util/resources/Vocabulary_fr.properties | 1 +
23 files changed, 861 insertions(+), 232 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractAssociation.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractAssociation.java
index eec56f1b88..b80093c16b 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractAssociation.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractAssociation.java
@@ -190,7 +190,7 @@ public abstract class AbstractAssociation extends
Field<Feature> implements Feat
final void ensureValid(final FeatureType base, final FeatureType type) {
if (base != type && !DefaultFeatureType.maybeAssignableFrom(base,
type)) {
throw new InvalidPropertyValueException(
- Resources.format(Resources.Keys.IllegalFeatureType_3,
getName(), base.getName(), type.getName()));
+ Resources.format(Resources.Keys.IllegalFeatureType_4, 0,
getName(), base.getName(), type.getName()));
}
}
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
index 3effa90bea..a8a65ec244 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
@@ -780,7 +780,7 @@ public abstract class AbstractFeature implements Feature,
Serializable {
private static String illegalFeatureType(
final FeatureAssociationRole association, final FeatureType
expected, final FeatureType actual)
{
- return Resources.format(Resources.Keys.IllegalFeatureType_3,
+ return Resources.format(Resources.Keys.IllegalFeatureType_4, 0,
association.getName(), expected.getName(),
actual.getName());
}
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 6c76e4c245..2fe724d6fe 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
@@ -155,7 +155,8 @@ public abstract class AbstractOperation extends
AbstractIdentifiedType implement
public abstract ParameterDescriptorGroup getParameters();
/**
- * Returns the expected result type, or {@code null} if none.
+ * Returns the expected result type.
+ * This type is normally not allowed to be null, but it may happen on
occasion.
*
* @return the type of the result, or {@code null} if none.
*/
@@ -183,7 +184,7 @@ public abstract class AbstractOperation extends
AbstractIdentifiedType implement
* in the Java language, and may be {@code null} if the operation does not
need a feature instance
* (like static methods in the Java language).</div>
*
- * @param feature the feature on which to execute the operation.
+ * @param instance the feature instance on which to execute the
operation.
* Can be {@code null} if the operation does not need
feature instance.
* @param parameters the parameters to use for executing the operation.
* Can be {@code null} if the operation does not take
any parameters.
@@ -191,7 +192,7 @@ public abstract class AbstractOperation extends
AbstractIdentifiedType implement
* @throws FeatureOperationException if the operation execution cannot
complete.
*/
@Override
- public abstract Property apply(Feature feature, ParameterValueGroup
parameters) throws FeatureOperationException;
+ public abstract Property apply(Feature instance, ParameterValueGroup
parameters) throws FeatureOperationException;
/**
* Returns the names of feature properties that this operation needs for
performing its task.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
index 35fe577163..485144073c 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
@@ -934,7 +934,7 @@ public class DefaultFeatureType extends
AbstractIdentifiedType implements Featur
return true;
}
if (super.equals(obj)) {
- final DefaultFeatureType that = (DefaultFeatureType) obj;
+ final var that = (DefaultFeatureType) obj;
return isAbstract == that.isAbstract &&
superTypes.equals(that.superTypes) &&
properties.equals(that.properties);
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
index 97b0d7c760..bfd7549cc7 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/FeatureTypeBuilder.java
@@ -39,6 +39,7 @@ import org.apache.sis.geometry.wrapper.Geometries;
import org.apache.sis.util.CorruptedObjectException;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Numbers;
+import org.apache.sis.util.OptionalCandidate;
import org.apache.sis.util.iso.DefaultNameFactory;
import org.apache.sis.util.resources.Errors;
@@ -653,6 +654,7 @@ public class FeatureTypeBuilder extends TypeBuilder {
*
* @see #addProperty(PropertyType)
*/
+ @OptionalCandidate
public PropertyTypeBuilder getProperty(final String name) {
return forName(properties, name, true);
}
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
index 93914b7037..2b38525bf8 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java
@@ -207,10 +207,10 @@ public class Resources extends IndexedResourceBundle {
public static final short IllegalCharacteristicsType_3 = 25;
/**
- * Association “{0}” does not accept features of type ‘{2}’. Expected
an instance of ‘{1}’ or
- * derived type.
+ * The “{1}” {0,choice,0#association|1#operation} expects features of
type ‘{2}’, but an
+ * instance of ‘{3}’ has been given.
*/
- public static final short IllegalFeatureType_3 = 26;
+ public static final short IllegalFeatureType_4 = 26;
/**
* Illegal grid envelope [{1,number} … {2,number}] for dimension {0}.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
index f03d695a80..3fc96f2d07 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties
@@ -49,7 +49,7 @@ GridEnvelopeMustBeNDimensional_1 = The grid envelope must
have at least {0} dim
GridExtentsAreDisjoint_5 = The specified grid extent is outside the
domain. Indices [{3,number} \u2026 {4,number}] specified in dimension {0} do
not intersect the [{1,number} \u2026 {2,number}] grid extent.
IllegalCategoryRange_2 = Sample value range {1} for \u201c{0}\u201d
category is illegal.
IllegalCharacteristicsType_3 = Expected an instance of \u2018{1}\u2019
for the \u201c{0}\u201d characteristics, but got an instance of \u2018{2}\u2019.
-IllegalFeatureType_3 = Association \u201c{0}\u201d does not
accept features of type \u2018{2}\u2019. Expected an instance of
\u2018{1}\u2019 or derived type.
+IllegalFeatureType_4 = The \u201c{1}\u201d
{0,choice,0#association|1#operation} expects features of type \u2018{2}\u2019,
but an instance of \u2018{3}\u2019 has been given.
IllegalGridEnvelope_3 = Illegal grid envelope [{1,number} \u2026
{2,number}] for dimension {0}.
IllegalGridGeometryComponent_1 = Cannot create a grid geometry with the
given \u201c{0}\u201d component.
IllegalPropertyType_2 = Type or result of \u201c{0}\u201d property
cannot be \u2018{1}\u2019 for this operation.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
index fa880920db..5610f695c7 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties
@@ -54,7 +54,7 @@ GridEnvelopeMustBeNDimensional_1 = L\u2019enveloppe de la
grille doit avoir au
GridExtentsAreDisjoint_5 = L\u2019\u00e9tendue de grille
sp\u00e9cifi\u00e9e est en dehors du domaine. Les indices [{3,number} \u2026
{4,number}] qui ont \u00e9t\u00e9 sp\u00e9cifi\u00e9es dans la dimension {0}
n\u2019interceptent pas l\u2019\u00e9tendue [{1,number} \u2026 {2,number}] de
la grille.
IllegalCategoryRange_2 = La plage de valeurs {1} pour la
cat\u00e9gorie \u00ab\u202f{0}\u202f\u00bb est ill\u00e9gale.
IllegalCharacteristicsType_3 = Une instance \u2018{1}\u2019 \u00e9tait
attendue pour la caract\u00e9ristique \u00ab\u202f{0}\u202f\u00bb, mais la
valeur donn\u00e9e est une instance de \u2018{2}\u2019.
-IllegalFeatureType_3 = L\u2019association
\u00ab\u202f{0}\u202f\u00bb n\u2019accepte pas les entit\u00e9s de type
\u2018{2}\u2019. Une instance de \u2018{1}\u2019 ou d\u2019un type
d\u00e9riv\u00e9 \u00e9tait attendue.
+IllegalFeatureType_4 =
L\u2019{0,choice,0#association|1#op\u00e9ration} \u00ab\u202f{1}\u202f\u00bb
attend des entit\u00e9s de type \u2018{2}\u2019, mais une instance de
\u2018{3}\u2019 a \u00e9t\u00e9 donn\u00e9e.
IllegalGridEnvelope_3 = La plage d\u2019index [{1,number} \u2026
{2,number}] de la dimension {0} n\u2019est pas valide.
IllegalGridGeometryComponent_1 = Ne peut pas construire une
g\u00e9om\u00e9trie de grille avec la composante \u00ab\u202f{0}\u202f\u00bb
donn\u00e9e.
IllegalPropertyType_2 = Le type ou le r\u00e9sultat de la
propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb ne peut pas \u00eatre
\u2018{1}\u2019 pour cette op\u00e9ration.
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
index 5efe2dc376..deac884387 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjection.java
@@ -25,6 +25,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
+import org.opengis.util.GenericName;
import org.apache.sis.util.Debug;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.resources.Vocabulary;
@@ -35,6 +36,7 @@ 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.feature.Operation;
import org.opengis.filter.Expression;
import org.opengis.filter.Literal;
import org.opengis.filter.ValueReference;
@@ -52,7 +54,15 @@ 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)}.
+ * features if aliases were specified by {@link
FeatureProjectionBuilder.Item#setPreferredName(GenericName)}.
+ *
+ * <h4>Relationship with {@code typeWithDependencies}</h4>
+ * The property <em>names</em> of {@code typeRequested} shall be a subset
of the property names of
+ * {@link #typeWithDependencies}. However, a property of the same name may
be an {@link Operation}
+ * in {@code typeWithDependencies} and replaced by {@link OperationView}
in {@code typeRequested}.
+ * This replacement is done by {@link
FeatureProjectionBuilder.Item#replaceIfMissingDependency()}.
+ * It is necessary because the {@link
org.apache.sis.feature.DefaultFeatureType} constructor
+ * verifies that all dependencies of all operations exist.
*/
public final FeatureType typeRequested;
@@ -60,6 +70,9 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
* 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).
+ * However, some operations may be wrapped in {@link OperationView}.
+ *
+ * @see #dependencies()
*/
public final FeatureType typeWithDependencies;
@@ -98,21 +111,21 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
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()];
+ 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];
+ int storedCount = 0;
for (final FeatureProjectionBuilder.Item item : projection) {
final var expression = item.attributeValueGetter();
if (expression != null) {
expressions[storedCount] = expression;
- propertiesToCopy[storedCount++] = item.getName();
+ propertiesToCopy[storedCount++] = item.getPreferredName();
}
}
this.propertiesToCopy = ArraysExt.resize(propertiesToCopy,
storedCount);
@@ -132,7 +145,7 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
* @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[])
+ * @see #forPreexistingFeatureInstances(int[])
*/
@SuppressWarnings({"rawtypes", "unchecked"})
private FeatureProjection(final FeatureProjection parent, final int[]
remaining) {
@@ -157,7 +170,7 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
* @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) {
+ public FeatureProjection forPreexistingFeatureInstances(final int[]
remaining) {
if (remaining.length == 0 && typeRequested == typeWithDependencies) {
return null;
}
@@ -166,6 +179,12 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
/**
* Creates a new projection with the same properties as the source
projection, but modified expressions.
+ * The modifications are specified by the given {@code mapper}. No
expression can be added or removed.
+ * New expressions should return values of the same type as the previous
expressions.
+ * The new expressions shall not introduce new dependencies.
+ *
+ * <h4>Purpose</h4>
+ * This constructor is used when the caller can replace some expressions
by <abbr>SQL</abbr> statements.
*
* @param source the projection to copy.
* @param mapper a function receiving in arguments the property name and
the expression fetching the property value,
@@ -241,7 +260,11 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
/**
* 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.
+ * The elements are XPaths to properties in the <em>source</em> features.
+ *
+ * <p>This method does not search for operations in {@link
#typeWithDependencies} because the operation
+ * dependencies are references to {@link #typeWithDependencies} properties
instead of properties of the
+ * source features. The property names may differ.</p>
*
* @return all dependencies (including transitive dependencies) as XPaths.
*/
@@ -255,13 +278,14 @@ public final class FeatureProjection implements
UnaryOperator<Feature> {
/**
* Derives a new projected feature instance from the given source.
- * The feature type of the returned feature instance will be be {@link
#typeRequested}.
+ * The given source feature should be of type {@link
#typeWithDependencies}.
+ * The type of the returned feature instance will 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>If this projection was created by {@link
#forPreexistingFeatureInstances(int[])},
+ * then the given feature shall be an instance of {@link
#typeWithDependencies} and may be modified.
+ * 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
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java
index c1e57566b3..17ca838849 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java
@@ -67,6 +67,8 @@ import org.opengis.filter.ValueReference;
* but they will receive no special treatment.</p>
*
* @author Martin Desruisseaux (Geomatys)
+ *
+ * @see #project()
*/
public final class FeatureProjectionBuilder extends FeatureTypeBuilder {
/**
@@ -107,8 +109,9 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
* 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>
+ * Keys are rather the values of {@code Item.builder.getPreferredName()},
+ * except that the latter may not be valid before {@link
Item#finish(Optimization)}
+ * has been invoked.</p>
*
* @see #reserve(GenericName, Item)
*/
@@ -117,12 +120,10 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
/**
* Whether at least one item is modified compared to the original property
in the source feature type.
* A modified item may be an item with a name different than the property
in {@linkplain #source}.
- * If {@code true}, then the projection cannot be an {@linkplain
#isIdentity() identity} operation.
* Result of operations such as links 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()
+ * @see Item#setPreferredName(GenericName)
*/
private boolean hasModifiedProperties;
@@ -133,7 +134,7 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
private int unnamedNumber;
/**
- * Names of {@linkplain #source} properties that are dependencies found in
operations.
+ * Names of {@linkplain #source} properties that are dependencies
(arguments) of feature 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.
*
@@ -144,15 +145,17 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
private final Map<String, List<Item>> dependencies;
/**
- * Whether to store operations as attributes. By default, when a
{@linkplain #addSourceProperty source
- * property is added in the projection}, operation are forwarded as given
(with their dependencies).
- * But if this flag is set to {@code true}, then operations are replaced
by an attribute.
- * Then, it will be caller's responsibility to store the value.
+ * Whether to store operations as attributes. By default, when {@linkplain
#addSourceProperty source
+ * properties are added in the projection}, operations are forwarded as
given (with their dependencies).
+ * But if this flag is set to {@code true}, then operations are replaced
by attributes.
+ * In such case, it will be caller's responsibility to store the value.
*/
private boolean operationResultAsAttribute;
/**
* Creates a new builder instance using the default factories.
+ * This constructor does <em>not</em> inherits the name of the given
feature type.
+ * Callers need to invoke a {@code setName(…)} method after construction.
*
* @todo provides a way to specify the factories used by the data store.
*
@@ -228,8 +231,8 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
* 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} set.
+ * it can produce a better error message). Operation's dependencies, if
any, are added into
+ * the given {@code deferred} collection.
*
* @param property the {@linkplain #source} property to add.
* @param deferred where to add operation's dependencies, or {@code
null} for not collecting dependencies.
@@ -271,12 +274,9 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
*
* @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.
+ * @return handler for the given item (never {@code null}).
*/
public Item addSourceProperty(final PropertyType property, final boolean
named) {
- if (property == null) {
- return null;
- }
final PropertyTypeBuilder builder;
final Collection<String> deferred;
if (sourceIsDependency) {
@@ -302,11 +302,22 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
builder = addPropertyResult(property, deferred);
}
final var item = new Item(named ? property.getName() : null, builder);
+ declareDependencies(item, deferred);
requested.add(item);
+ return item;
+ }
+
+ /**
+ * Declares that the {@code deferred} elements are dependencies of the
given item.
+ * This information will be used by {@link #resolveDependencies(List)}.
+ *
+ * @param item the item having dependencies.
+ * @param deferred the item dependencies.
+ */
+ private void declareDependencies(final Item item, final Collection<String>
deferred) {
for (String dependency : deferred) {
dependencies.computeIfAbsent(dependency, (key) -> new
ArrayList<>(2)).add(item);
}
- return item;
}
/**
@@ -319,15 +330,12 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
* It also verifies that no {@link Item} have been created for that
builder yet.
* For performance reasons, those verifications are performed only if
assertions are enabled.
*
- * @param builder builder for the computed property, or {@code null}.
+ * @param builder builder for the computed property.
* @param named whether the {@code builder} name can be used as a
default name.
- * @return handler for the given item, or {@code null} if the given
builder was null.
+ * @return handler for the given item (should never be {@code null}).
*/
@ArgumentCheckByAssertion
public Item addComputedProperty(final PropertyTypeBuilder builder, final
boolean named) {
- if (builder == null) {
- return null;
- }
assert properties().contains(builder) : builder;
assert requested.stream().noneMatch((item) -> item.builder == builder)
: builder;
final var item = new Item(named ? builder.getName() : null, builder);
@@ -335,9 +343,58 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
return item;
}
+ /**
+ * Adds dependencies. This method adds into 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).
+ * @return whether there is no dependency to resolve (i.e., all
dependencies are already resolved).
+ * @throws UnsupportedOperationException if there is an attempt to rename
a property which is used by an operation.
+ */
+ private boolean resolveDependencies(final List<PropertyType> deferred) {
+ final Iterator<Map.Entry<String, List<Item>>> 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();
+ final Item item = reservedNames.get(sourceName);
+ if (item != null) {
+ if (!sourceName.equals(item.sourceName)) {
+ throw new
UnsupportedOperationException(Resources.forLocale(getLocale())
+
.getString(Resources.Keys.CannotRenameDependency_2, item.sourceName,
sourceName));
+ }
+ } else {
+ for (Item dependent : entry.getValue()) {
+ dependent.hasMissingDependency = true;
+ }
+ deferred.add(property);
+ }
+ it.remove();
+ }
+ return deferred.isEmpty();
+ }
+
+ /**
+ * Declares that the given name is reserved. If this class needs to
generate a default name,
+ * it will ensure that any automatically generated name 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);
+ }
+ }
+ }
+
/**
* 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)}.
+ * A name can be specified explicitly after construction by {@link
#setPreferredName(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.
*/
@@ -385,6 +442,15 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
*/
private Expression<? super Feature, ?> attributeValueGetter;
+ /**
+ * Whether the {@link #attributeValueGetter} should be executed only
when the property value is requested.
+ * If {@code false} (the default), the value is computed from the
source feature exactly once, when the
+ * {@link FeatureProjection#apply(Feature)} method is invoked, and the
result is stored as an attribute.
+ * If {@code true}, then the expression is wrapped in an {@link
AbstractOperation} and evaluated when
+ * the property value is requested.
+ */
+ private boolean isDeferredValueGetter;
+
/**
* Creates a new handle for the property created by the given builder.
*
@@ -403,9 +469,10 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
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);
+ "targetName", isNamed ? getPreferredName() : null,
+ "valueClass", getValueClass(),
+ null, hasMissingDependency ? "hasMissingDependency" :
null,
+ null, isDeferredValueGetter ? "isDeferredValueGetter" :
null);
}
/**
@@ -421,7 +488,7 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
* Use the dedicated methods in this class instead:
*
* <ul>
- * <li>Set the name: use {@link #setName(GenericName)}.</li>
+ * <li>Set the name: use {@link #setPreferredName(GenericName)}.</li>
* <li>Set the value class: use {@link
#replaceValueClass(UnaryOperator)}.</li>
* </ul>
*
@@ -433,18 +500,20 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
}
/**
- * 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.
+ * Replaces this property by a view that will not block the creation
of {@code DefaultFeatureType}
+ * when a dependency is missing. This method should be invoked only
for creating the feature type
+ * to be shown to users through {@link FeatureView}.
*/
private void replaceIfMissingDependency() {
if (hasMissingDependency) {
hasMissingDependency = false;
hasModifiedProperties = true;
- final var old = builder;
- builder = addPropertyResult(old.build(), null); //
`old.build()` returns the existing operation.
- old.replaceBy(builder);
+ final PropertyTypeBuilder old = builder;
+ final PropertyType property = old.build(); // `old.build()`
returns the existing operation.
+ if (property instanceof AbstractOperation) {
+ builder = addProperty(new
OperationView((AbstractOperation) property, getName()));
+ old.replaceBy(builder);
+ }
}
}
@@ -454,16 +523,21 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
* 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}.
*
+ * <p>Note that when {@code type.apply(valueClass)} is invoked,
returning {@code null} is not the same
+ * as returning the {@code valueClass} unchanged, even if this {@code
Item} is unchanged in both cases.
+ * This method returns {@code false} in the former case while it
returns {@code true} if the latter case.</p>
+ *
* @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}.
+ * @return whether the value returned by {@code type} has been
accepted (not necessarily causing a change of state).
* @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) {
- if (builder != (builder = ab.setValueClass(r))) {
+ final Class<?> oldType = ab.getValueClass();
+ final Class<?> newType = type.apply(oldType);
+ if (newType != null) {
+ if (builder != (builder = ab.setValueClass(newType))) {
hasModifiedProperties = true;
}
return true;
@@ -480,18 +554,18 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
*/
final var result = ((Operation) property).getResult();
if (result instanceof AttributeType<?>) {
- final Class<?> c = ((AttributeType<?>)
result).getValueClass();
- final Class<?> r = type.apply(c);
- if (r != null) {
+ final Class<?> oldType = ((AttributeType<?>)
result).getValueClass();
+ final Class<?> newType = type.apply(oldType);
+ if (newType != null) {
/*
* We can be lenient for link operation, but must
be strict for other operations.
* Example: a link to a geometry, but relaxing the
`Polygon` type to `Geometry`.
*/
- if (Features.getLinkTarget(property).isPresent() ?
r.isAssignableFrom(c) : r.equals(c)) {
+ if (Features.getLinkTarget(property).isPresent() ?
newType.isAssignableFrom(oldType) : newType.equals(oldType)) {
return true;
}
throw new
UnconvertibleObjectException(Errors.forLocale(getLocale())
-
.getString(Errors.Keys.CanNotConvertFromType_2, c, r));
+
.getString(Errors.Keys.CanNotConvertFromType_2, oldType, newType));
}
}
}
@@ -499,47 +573,30 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
return false;
}
+ /**
+ * Returns the class of attribute values currently configured in the
builder, or {@code null} if none.
+ */
+ private Class<?> getValueClass() {
+ return (builder instanceof AttributeTypeBuilder<?>) ?
((AttributeTypeBuilder<?>) builder).getValueClass() : null;
+ }
+
/**
* 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.
+ * If {@code stored} is {@code false}, this expression will be wrapped
in an operation which will
+ * be executed when the property value is requested. Note that in the
latter case, this builder
+ * may have to retain more dependencies than what the user specified.
*
* @param expression the expression to be evaluated by the operation.
+ * @param stored {@code true} for evaluating the expression
immediately after feature instances
+ * are known, or {@code false} for wrapping the
expression in a feature operation.
*/
public void setValueGetter(final Expression<? super Feature, ?>
expression, final boolean stored) {
+ // Modify only direct value references, not link (or other
operations).
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);
- hasModifiedProperties = true;
- }
- } else {
- // The property is an operation, usually a link. Leave it
as-is.
+ attributeValueGetter = expression;
+ isDeferredValueGetter = !stored;
}
}
@@ -552,13 +609,6 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
return attributeValueGetter;
}
- /**
- * Optimizes the expression. This is invoked as the last step before
to build the final feature projection.
- */
- final void optimize(final Optimization optimizer) {
- attributeValueGetter = optimizer.apply(attributeValueGetter);
- }
-
/**
* Sets the coordinate reference system that characterizes the values
of this attribute.
*
@@ -581,8 +631,6 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
*
* @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());
@@ -591,11 +639,11 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
/**
* 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)}.
+ * but can be changed later by a call to {@link
#setPreferredName(GenericName)}.
*
* @return the name of the projected property.
*/
- public String getName() {
+ public String getPreferredName() {
return builder.getName().toString();
}
@@ -608,7 +656,7 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
*
* @param targetName the desired name in the projected feature, or
{@code null} if unspecified.
*/
- public void setName(final GenericName targetName) {
+ public void setPreferredName(final GenericName targetName) {
if (targetName == null) {
reserve(sourceName, null); // Will use that name only if
not owned by another item.
preferCurrentName = true;
@@ -624,12 +672,19 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
}
/**
+ * Completes and optimizes this item before construction of the final
{@link FeatureProjection}.
+ *
+ * <h4>Naming</h4>
* 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.
+ * This method should be invoked only after {@link
#setPreferredName(GenericName)}
+ * has been invoked for all items, for allowing this class to know
which names are reserved.
+ *
+ * <h4>Optimization</h4>
+ * This method ensures that the {@link #attributeValueGetter}
expression produces a value of the type
+ * expected by {@link FeatureProjection#typeWithDependencies}, adding
a conversion step if needed.
+ * Then this method simplifies the expression, which may remove the
conversion added just before.
*/
- private void validateName() {
+ final void finish(final Optimization optimizer) {
if (!isNamed) {
final Item owner = reservedNames.get(sourceName);
if (owner != this) {
@@ -644,83 +699,46 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
}
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).
- * @throws UnsupportedOperationException if there is an attempt to rename
a property which is used by an operation.
- */
- 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)) {
- throw new
UnsupportedOperationException(Resources.forLocale(getLocale())
-
.getString(Resources.Keys.CannotRenameDependency_2, item.sourceName,
sourceName));
+ if (attributeValueGetter != null) {
+ final Class<?> valueClass = getValueClass();
+ if (valueClass != null) {
+ attributeValueGetter =
attributeValueGetter.toValueType(valueClass);
}
- } else {
- for (Item dependent : entry.getValue()) {
- dependent.hasMissingDependency = true;
+ attributeValueGetter = optimizer.apply(attributeValueGetter);
+ if (isDeferredValueGetter) {
+ // Following cast should never fail because it was
verified in `setValueGetter(…)`.
+ 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
+ * (a replacement by a `LinkOperation`) in
`ExpressionOperation.create(…)`.
+ */
+ AttributeType<?> storedType = null;
+ if (attributeValueGetter instanceof ValueReference<?,?>) {
+ var candidate =
source.getProperty(((ValueReference<?,?>) attributeValueGetter).getXPath());
+ if (candidate instanceof AttributeType<?>) {
+ storedType = (AttributeType<?>) candidate;
+ }
+ }
+ if (storedType == null) {
+ storedType = atb.build(); // Same name as in the
`identification` map below.
+ }
+ final Map<String, ?> identification =
Map.of(AbstractOperation.NAME_KEY, atb.getName());
+ final var operation =
FeatureOperations.expression(identification, attributeValueGetter, storedType);
+ if (operation instanceof AbstractOperation) { // Should
always be the case, but be safe.
+ declareDependencies(this, ((AbstractOperation)
operation).getDependencies());
+ builder = addProperty(operation);
+ atb.replaceBy(builder);
+ hasModifiedProperties = true;
+ attributeValueGetter = null;
+ }
}
- 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 (hasModifiedProperties) {
- 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
@@ -738,45 +756,65 @@ public final class FeatureProjectionBuilder extends
FeatureTypeBuilder {
* @throws UnsupportedOperationException if there is an attempt to rename
a property which is used by an operation.
*/
public Optional<FeatureProjection> project() {
- requested.forEach(Item::validateName);
+ final var optimizer = new Optimization();
+ optimizer.setFeatureType(source);
+ requested.forEach((item) -> item.finish(optimizer));
/*
* Add properties for all dependencies that are required by operations
but are not already present.
- * If there is no need to add anything, `typeWithDependencies` will be
directly the feature type to return.
+ * If there is no need to add anything, `typeWithDependencies` will be
directly `typeRequested`.
*/
+ final FeatureType typeRequested, typeWithDependencies;
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()) {
+ while (!resolveDependencies(deferred)) {
+ for (PropertyType property : deferred) {
+ final Item item = addSourceProperty(property, true);
+ if (item != null) {
+
item.setValueGetter(FeatureOperations.expressionOf(property), true);
+ item.finish(optimizer);
+ }
+ }
+ deferred.clear();
+ }
+ final List<PropertyTypeBuilder> added = properties.subList(count,
properties.size());
+ if (added.isEmpty()) {
typeRequested = typeWithDependencies = build();
} else {
- do {
- for (PropertyType property : deferred) {
- final Item item = addSourceProperty(property, true);
- if (item != null) {
- 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.
+ final GenericName name = getName();
+ typeWithDependencies = setName(
+ Vocabulary.formatInternational(
+ Vocabulary.Keys.PlusDependencies_1,
+ name.tip().toInternationalString()))
+ .build();
+
+ added.clear(); // Keep only the properties requested by user.
requested.forEach(Item::replaceIfMissingDependency);
- typeRequested = build();
+ typeRequested = setName(name).build();
}
- if (source.equals(typeRequested) &&
source.equals(typeWithDependencies)) {
+ if (typeRequested.equals(source)) {
return Optional.empty();
}
- final var optimizer = new Optimization();
- optimizer.setFeatureType(source);
- requested.forEach((item) -> item.optimize(optimizer));
return Optional.of(new FeatureProjection(typeRequested,
typeWithDependencies, requested));
}
+
+ /**
+ * Returns the feature type described by this builder. This method may
return the
+ * {@linkplain #source() source} directly if this projection performs no
operation.
+ *
+ * <p>Users should not invoke this method directly. Use {@link #project()}
instead.</p>
+ */
+ @Override
+ public FeatureType build() {
+ if (hasModifiedProperties) {
+ return super.build();
+ }
+ final Iterator<Item> it = requested.iterator();
+ for (PropertyType property : source.getProperties(true)) {
+ if (!(it.hasNext() && it.next().equivalent(property))) {
+ return super.build();
+ }
+ }
+ return it.hasNext() ? super.build() : source;
+ }
}
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/OperationView.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/OperationView.java
new file mode 100644
index 0000000000..ddbeebe36b
--- /dev/null
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/OperationView.java
@@ -0,0 +1,192 @@
+/*
+ * 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.internal.shared;
+
+import java.util.Optional;
+import java.io.Serializable;
+import org.opengis.util.GenericName;
+import org.opengis.util.InternationalString;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.apache.sis.feature.AbstractOperation;
+import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.util.Deprecable;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureOperationException;
+import org.opengis.feature.IdentifiedType;
+import org.opengis.feature.Operation;
+import org.opengis.feature.Property;
+import org.opengis.feature.PropertyNotFoundException;
+
+
+/**
+ * An operation wrapper for hiding its dependencies. Because Apache
<abbr>SIS</abbr>
+ * fetches operation dependencies only from instances of {@link
AbstractOperation},
+ * we only need a wrapper that do <em>not</em> extend {@code
AbstractOperation}.
+ * This class should be used only together with {@link FeatureView}.
+ *
+ * <h2>Purpose</h2>
+ * The {@link DefaultFeatureType} constructor verifies that all dependencies
of all operations exist.
+ * This verification can block us from constructing the {@link
FeatureView#source} type if the view
+ * does not include all dependencies needed by an operation. By wrapping the
operation, we prevent
+ * {@link DefaultFeatureType} from doing this verification.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+final class OperationView implements Operation, Deprecable, Serializable {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = -4679426189917900959L;
+
+ /**
+ * The instance doing the actual operation.
+ */
+ private final AbstractOperation source;
+
+ /**
+ * The name of feature type expected by the operation.
+ */
+ @SuppressWarnings("serial") // Most Apache SIS implementations are
serializable.
+ private final GenericName valueType;
+
+ /**
+ * Creates a new operation wrapper.
+ *
+ * @param valueType the name of feature type expected by the operation.
+ * @param source the operation instance to make opaque.
+ */
+ OperationView(final AbstractOperation source, final GenericName valueType)
{
+ this.source = source;
+ this.valueType = valueType;
+ }
+
+ /**
+ * Returns the name of the wrapped operation.
+ */
+ @Override
+ public GenericName getName() {
+ return source.getName();
+ }
+
+ /**
+ * Returns a concise definition of the operation.
+ */
+ @Override
+ public InternationalString getDefinition() {
+ return source.getDefinition();
+ }
+
+ /**
+ * Returns a natural language designator for the operation.
+ * This can be used as an alternative to the {@linkplain #getName() name}
in user interfaces.
+ */
+ @Override
+ public Optional<InternationalString> getDesignation() {
+ return source.getDesignation();
+ }
+
+ /**
+ * Returns optional information beyond that required for concise
definition of the element.
+ * The description may assist in understanding the element scope and
application.
+ */
+ @Override
+ public Optional<InternationalString> getDescription() {
+ return source.getDescription();
+ }
+
+ /**
+ * If this instance is deprecated, the reason or the alternative to use.
+ */
+ @Override
+ public Optional<InternationalString> getRemarks() {
+ return source.getRemarks();
+ }
+
+ /**
+ * Returns {@code true} if this instance is deprecated.
+ */
+ @Override
+ public boolean isDeprecated() {
+ return source.isDeprecated();
+ }
+
+ /**
+ * Returns a description of the input parameters.
+ */
+ @Override
+ public ParameterDescriptorGroup getParameters() {
+ return source.getParameters();
+ }
+
+ /**
+ * Returns the expected result type.
+ */
+ @Override
+ public IdentifiedType getResult() {
+ return source.getResult();
+ }
+
+ /**
+ * Executes the operation on the specified feature with the specified
parameters.
+ * This method is not strictly compliant with the contract of the public
interface,
+ * because it requires that the given feature is an instance of {@link
#valueType}.
+ * If not the case, this method tries to produce a more helpful exception
message.
+ *
+ * @param instance the feature instance on which to execute the
operation.
+ * Can be {@code null} if the operation does not need
feature instance.
+ * @param parameters the parameters to use for executing the operation.
+ * Can be {@code null} if the operation does not take
any parameters.
+ * @return the operation result.
+ */
+ @Override
+ public Property apply(final Feature instance, final ParameterValueGroup
parameters) {
+ try {
+ return source.apply(instance, parameters);
+ } catch (PropertyNotFoundException e) {
+ throw new FeatureOperationException(Resources.format(
+ Resources.Keys.IllegalFeatureType_4, 1, getName(),
valueType, instance.getType().getName()), e);
+ }
+ }
+
+ /**
+ * Returns a string representation of this operation.
+ */
+ @Override
+ public String toString() {
+ return source.toString();
+ }
+
+ /**
+ * Returns a hash code value for this operation.
+ */
+ @Override
+ public int hashCode() {
+ return source.hashCode() * 37;
+ }
+
+ /**
+ * Compares this operation with the given object for equality.
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ return (obj instanceof OperationView) &&
source.equals(((OperationView) obj).source);
+ }
+}
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 c4d56289c6..f3a521ddd1 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
@@ -191,12 +191,14 @@ abstract class PropertyValue<V> extends
LeafExpression<Feature,V>
// `getResultClass()` should never return null with our subtypes of
`PropertyValue`.
if (target == getResultClass()) {
return (PropertyValue<N>) this;
+ } else if (target == Object.class) {
+ return (PropertyValue<N>) new AsObject(name, isVirtual);
}
final Class<?> source = getSourceClass();
- if (target == Object.class) {
- return (PropertyValue<N>) new AsObject(name, isVirtual);
- } else if (source == Object.class) {
+ if (source == Object.class) {
return new Converted<>(target, name, isVirtual);
+ } else if (target.isAssignableFrom(source)) {
+ return new Unsafe<>(source, target, name, isVirtual);
} else {
return new CastedAndConverted<>(source, target, name, isVirtual);
}
@@ -229,7 +231,7 @@ abstract class PropertyValue<V> extends
LeafExpression<Feature,V>
}
/**
- * Returns the type of objects retuned by this expression.
+ * Returns the type of objects returned by this expression.
*/
@Override
public Class<Object> getResultClass() {
@@ -275,7 +277,7 @@ abstract class PropertyValue<V> extends
LeafExpression<Feature,V>
/**
* An expression fetching property values as an object of specified type.
- * The value is converted from {@link Object} to the specified type.
+ * The value is converted from an arbitrary {@link Object} to an instance
of the specified type.
*
* @param <V> the type of value computed by the expression.
*/
@@ -288,6 +290,8 @@ abstract class PropertyValue<V> extends
LeafExpression<Feature,V>
/**
* Creates a new expression retrieving values from a property of the
given name.
+ * The {@code type} argument should never be {@code Object.class},
otherwise an
+ * {@link AsObject} should have been constructed instead.
*
* @param type the desired type for the expression result.
* @param name the name of the property to fetch.
@@ -299,6 +303,7 @@ abstract class PropertyValue<V> extends
LeafExpression<Feature,V>
/**
* Returns the type of values computed by this expression.
+ * Should be a subtype of {@link Object}, never {@code Object} itself.
*/
@Override
public final Class<V> getResultClass() {
@@ -356,6 +361,8 @@ abstract class PropertyValue<V> extends
LeafExpression<Feature,V>
if (!(rename.equals(name) && source.equals(original))) {
if (source == Object.class) {
return new Converted<>(type, rename, isVirtual);
+ } else if (type.isAssignableFrom(source)) {
+ return new Unsafe<>(source, type, rename, isVirtual);
} else {
return new CastedAndConverted<>(source, type, rename,
isVirtual);
}
@@ -373,6 +380,8 @@ abstract class PropertyValue<V> extends
LeafExpression<Feature,V>
* the original attribute type is kept unchanged because {@link
#apply(Feature)}
* does not convert those values.
*
+ * @param addTo where to add the type of the property evaluated by
this expression.
+ * @return handler of the added property (never {@code null}).
* @throws UnconvertibleObjectException if the property default value
cannot be converted to {@link #type}.
*/
@Override
@@ -393,8 +402,8 @@ abstract class PropertyValue<V> extends
LeafExpression<Feature,V>
* 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.
+ * @param addTo where to add the type of the property evaluated by this
expression.
+ * @return handler of the added property (never {@code null}).
* @throws PropertyNotFoundException if the property was not found in
{@code addTo.source()}.
*/
@Override
@@ -434,7 +443,11 @@ abstract class PropertyValue<V> extends
LeafExpression<Feature,V>
@SuppressWarnings("serial") // Most SIS implementations are
serializable.
private final ObjectConverter<? super S, ? extends V> converter;
- /** Creates a new expression retrieving values from a property of the
given name. */
+ /**
+ * Creates a new expression retrieving values from a property of the
given name.
+ * The {@code type} argument should never be {@code Object.class},
otherwise an
+ * {@link AsObject} should have been constructed instead.
+ */
CastedAndConverted(final Class<S> source, final Class<V> type, final
String xpath, final boolean isVirtual) {
super(type, xpath, isVirtual);
this.source = source;
@@ -463,4 +476,56 @@ abstract class PropertyValue<V> extends
LeafExpression<Feature,V>
return null;
}
}
+
+
+
+ /**
+ * An expression skipping the conversion step because the features already
provide instances of the desired type.
+ * This variant is said "unsafe" because it trusts that the feature
instances guarantee that the property values
+ * are instances of the class declared in the {@code FeatureType}. If this
assumption is wrong (which would be a
+ * bug in the caller's code rather than this class), a {@link
ClassCastException} will probably be thrown anyway
+ * but later, possibly in a bridge method generated by the compiler for
generic types.
+ *
+ * @param <S> the type of source value before conversion.
+ * @param <V> the type of value computed by the expression.
+ */
+ private static final class Unsafe<S,V> extends Converted<V> {
+ /** For cross-version compatibility. */
+ private static final long serialVersionUID = -223028669950189532L;
+
+ /** The type of values fetched from the feature instance. */
+ private final Class<S> source;
+
+ /**
+ * Creates a new expression retrieving values from a property of the
given name.
+ * The {@code type} argument should never be {@code Object.class},
otherwise an
+ * {@link AsObject} should have been constructed instead.
+ */
+ Unsafe(final Class<S> source, final Class<V> type, final String xpath,
final boolean isVirtual) {
+ super(type, xpath, isVirtual);
+ this.source = source;
+ }
+
+ /** Returns the type of values fetched from {@link Feature} instance.
*/
+ @Override
+ protected Class<S> getSourceClass() {
+ return source;
+ }
+
+ /**
+ * Returns the value of the property of the given name, or {@code
null} if none.
+ * For performance reason, this method does not verify the value type
on the
+ * assumption that the type will be verified again by the caller
anyway.
+ */
+ @Override
+ @SuppressWarnings("unchecked")
+ public V apply(final Feature instance) {
+ if (instance != null) try {
+ return (V) instance.getPropertyValue(name);
+ } catch (PropertyNotFoundException e) {
+ warning(e, false);
+ }
+ return null;
+ }
+ }
}
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/ConvertFunction.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/ConvertFunction.java
index cdce085ed6..802f293207 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/ConvertFunction.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/ConvertFunction.java
@@ -170,6 +170,8 @@ public final class ConvertFunction<R,S,V> extends
UnaryFunction<R,S>
* Provides the type of values produced by this expression.
* May return {@code null} if the type cannot be determined.
*
+ * @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 UnconvertibleObjectException if the property default value
cannot be converted to the expected type.
*/
@Override
@@ -179,7 +181,7 @@ public final class ConvertFunction<R,S,V> extends
UnaryFunction<R,S>
return null;
}
final FeatureProjectionBuilder.Item item =
addTo.addTemplateProperty(fex);
- item.replaceValueClass((c) -> getResultClass());
+ if (item != null) item.replaceValueClass((c) -> getResultClass());
return item;
}
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Function.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Function.java
index 9beeb396b6..40ef4d856d 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Function.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/math/Function.java
@@ -267,6 +267,8 @@ public enum Function implements FunctionIdentifier,
AvailableFunction {
/**
* Returns the attribute type to declare in feature types that store
result of this function.
+ *
+ * @return the attribute type (never {@code null}).
*/
final synchronized AttributeType<?> getResultType() {
if (resultType == null) {
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 d031ff73d7..3542baf1b3 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
@@ -213,12 +213,14 @@ search: if (crs instanceof CoordinateReferenceSystem) {
* 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 addTo where to add the type of properties evaluated by this
expression.
- * @return handler of type resulting from expression evaluation (never
null).
+ * @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.
*/
@Override
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);
+ FeatureProjectionBuilder.Item item = super.expectedType(addTo);
+ if (item != null) item.setCRS(literalCRS ? targetCRS : null);
+ return item;
}
}
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 c6ffa0a876..2e0756c7e2 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
@@ -180,8 +180,8 @@ abstract class SpatialFunction<R> extends Node
* <li>Otherwise, an attribute is created with the return value
specified by the operation.</li>
* </ul>
*
- * @param addTo where to add the type of properties evaluated by this
expression.
- * @return builder of type resulting from expression evaluation (never
null).
+ * @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 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.
@@ -192,11 +192,11 @@ abstract class SpatialFunction<R> extends Node
final FeatureExpression<?,?> fex =
FeatureExpression.castOrCopy(getParameters().get(0));
if (fex != null) {
final FeatureProjectionBuilder.Item item =
addTo.addTemplateProperty(fex);
- final boolean success = item.replaceValueClass((c) -> {
+ final boolean accept = (item == null) ||
item.replaceValueClass((c) -> {
final Geometries<?> library = Geometries.factory(c);
return (library == null) ? null :
operation.getReturnType(library);
});
- if (success) {
+ if (accept) {
return item;
}
}
diff --git
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
new file mode 100644
index 0000000000..282f42986c
--- /dev/null
+++
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/internal/shared/FeatureProjectionTest.java
@@ -0,0 +1,294 @@
+/*
+ * 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.internal.shared;
+
+import java.util.Set;
+import java.util.HashSet;
+import org.apache.sis.util.iso.Names;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.filter.DefaultFilterFactory;
+
+// 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.Operation;
+import org.opengis.filter.FilterFactory;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.apache.sis.test.Assertions.assertSetEquals;
+import org.apache.sis.test.TestCase;
+
+
+/**
+ * Tests {@link FeatureProjection} and {@link FeatureProjectionBuilder}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public final class FeatureProjectionTest extends TestCase {
+ /**
+ * The feature type to use as a source, before projection.
+ */
+ private final FeatureType source;
+
+ /**
+ * The factory to use for building expressions.
+ */
+ private final FilterFactory<Feature, ?, ?> ff;
+
+ /**
+ * Creates a new test case.
+ */
+ public FeatureProjectionTest() {
+ ff = DefaultFilterFactory.forFeatures();
+ var builder = new FeatureTypeBuilder().setName("Country");
+ builder.addAttribute(String .class).setName("name");
+ builder.addAttribute(String .class).setName("capital");
+ builder.addAttribute(Integer.class).setName("population");
+ builder.addAttribute(Double .class).setName("area");
+ source = builder.build();
+ }
+
+ /**
+ * Tests the creation of a subset on a simple feature type.
+ * The type of values are not changed.
+ */
+ @Test
+ public void testSubsetWithSameValueClasses() {
+ final var builder = new FeatureProjectionBuilder(source, null);
+ addCountry(builder);
+ addPopulation(builder, false);
+ final FeatureProjection projection =
assertContryAndPopulation(builder);
+ assertValueClassEquals(Integer.class, projection, "population");
+ final Feature result = applyOnFeatureInstance(projection);
+ assertEquals("Canada", result.getPropertyValue("country"));
+ assertEquals(40769890, result.getPropertyValue("population"));
+ }
+
+ /**
+ * Tests the creation of a subset on a simple feature type with a change
of property type.
+ * The type of the population value is changed from {@code Integer} to
{@code Long}.
+ */
+ @Test
+ public void testSubsetWithModifiedValueClasses() {
+ final var builder = new FeatureProjectionBuilder(source, null);
+ addCountry(builder);
+ addPopulation(builder, true);
+ final FeatureProjection projection =
assertContryAndPopulation(builder);
+ assertValueClassEquals(Long.class, projection, "population");
+ final Feature result = applyOnFeatureInstance(projection);
+ assertEquals("Canada", result.getPropertyValue("country"));
+ assertEquals(40769890L, result.getPropertyValue("population"));
+ }
+
+ /**
+ * Creates a feature instance, applies the projection and returns the
result.
+ *
+ * @param projection the projection to apply.
+ * @return the projected feature instance.
+ */
+ private Feature applyOnFeatureInstance(final FeatureProjection projection)
{
+ final Feature instance = source.newInstance();
+ instance.setPropertyValue("name", "Canada");
+ instance.setPropertyValue("area", 9984670d);
+ instance.setPropertyValue("capital", "Ottawa");
+ instance.setPropertyValue("population", 40769890); // In 2024.
+ final Feature result = projection.apply(instance);
+ assertNotEquals(instance, result);
+ return result;
+ }
+
+ /**
+ * Builds the projection and asserts that it contains a country name and a
population.
+ *
+ * @param builder the builder.
+ * @return the projection built by the given builder.
+ */
+ private FeatureProjection assertContryAndPopulation(final
FeatureProjectionBuilder builder) {
+ assertSame(source, builder.source());
+ builder.setName("Country population");
+ final FeatureProjection projection = builder.project().orElseThrow();
+ assertSame(projection.typeRequested, projection.typeWithDependencies);
+ assertPropertyNamesEqual(projection.typeRequested, "country",
"population");
+ assertSetEquals(Set.of("name", "population"),
projection.dependencies());
+ assertValueClassEquals(String.class, projection, "country");
+ return projection;
+ }
+
+ /**
+ * Asserts that the given feature has properties of the given names in the
same order.
+ */
+ private static void assertPropertyNamesEqual(final FeatureType type, final
String... expected) {
+ assertArrayEquals(expected, type.getProperties(true).stream().map((p)
-> p.getName().toString()).toArray());
+ }
+
+ /**
+ * Asserts that the given property is an attribute with the specified
value type.
+ *
+ * @param expected expected value class.
+ * @param projection the projection to verify.
+ * @param property the property to verify in the given projection.
+ */
+ private static void assertValueClassEquals(final Class<?> expected, final
FeatureProjection projection, final String property) {
+ assertEquals(expected, assertInstanceOf(AttributeType.class,
projection.typeRequested.getProperty(property)).getValueClass());
+ }
+
+ /**
+ * Adds a projection item for the country name.
+ * The property is renamed from "name" to "country".
+ *
+ * @param builder the builder in which to add the item.
+ */
+ private void addCountry(final FeatureProjectionBuilder builder) {
+ final FeatureProjectionBuilder.Item item =
builder.addSourceProperty(source.getProperty("name"), true);
+ assertEquals("name", item.sourceName.toString());
+ assertEquals("name", item.getPreferredName());
+ item.setPreferredName(Names.createLocalName(null, null, "country"));
+ assertEquals("country", item.getPreferredName());
+ assertNull(item.attributeValueGetter());
+ item.setValueGetter(ff.property("name"), true);
+ }
+
+ /**
+ * Adds a projection item for the population, optionally with a change of
the value type.
+ *
+ * @param builder the builder in which to add the item.
+ * @param changeType whether to change the population type from {@code
Integer} to {@code Long}.
+ */
+ private void addPopulation(final FeatureProjectionBuilder builder, final
boolean changeType) {
+ final FeatureProjectionBuilder.Item item =
builder.addSourceProperty(source.getProperty("population"), true);
+ assertEquals("population", item.sourceName.toString());
+ assertEquals("population", item.getPreferredName());
+ assertNull(item.attributeValueGetter());
+ item.setValueGetter(ff.property("population"), true);
+ assertTrue(item.replaceValueClass((type) -> {
+ assertEquals(Integer.class, type);
+ return changeType ? Long.class : type;
+ }));
+ }
+
+ /**
+ * Adds an expression which computes the population density from two other
properties.
+ *
+ * @param builder the builder in which to add the item.
+ * @param stored {@code true} for evaluating the expression immediately
after feature instances
+ * are known, or {@code false} for wrapping the expression
in a feature operation.
+ */
+ private void addPopulationDensity(final FeatureProjectionBuilder builder,
final boolean stored) {
+ final FeatureProjectionBuilder.Item item = builder.addComputedProperty(
+ builder.addAttribute(Number.class).setName("density"), true);
+ assertEquals("density", item.sourceName.toString());
+ assertEquals("density", item.getPreferredName());
+ assertNull(item.attributeValueGetter());
+
item.setValueGetter(ff.divide(ff.property("population").toValueType(Number.class),
+
ff.property("area").toValueType(Number.class)), stored);
+ }
+
+ /**
+ * Tests the creation of a feature with an additional property computed
early.
+ * The operation is computed immediately from the source feature type.
+ */
+ @Test
+ public void testSubsetWithStoredOperation() {
+ final var builder = new FeatureProjectionBuilder(source, null);
+ addCountry(builder);
+ addPopulation(builder, false);
+ addPopulationDensity(builder, true);
+ builder.setName("Population density");
+ final FeatureProjection projection = builder.project().orElseThrow();
+ assertSame(projection.typeRequested, projection.typeWithDependencies);
+ assertSetEquals(Set.of("name", "population", "area"),
projection.dependencies());
+ assertPropertyNamesEqual(projection.typeRequested, "country",
"population", "density");
+
+ // Property is an attribute because we requested the "stored" mode.
+ assertInstanceOf(AttributeType.class,
projection.typeWithDependencies.getProperty("density"));
+ assertValueClassEquals(Number.class, projection, "density");
+ verifyDensityOnFeatureInstance(projection);
+ }
+
+ /**
+ * Tests the creation of a feature with an additional property computed
when first requested.
+ * The operation forces the projection to add dependencies that were not
part of the request.
+ */
+ @Test
+ public void testSubsetWithDeferredOperation() {
+ final var builder = new FeatureProjectionBuilder(source, null);
+ addCountry(builder);
+ addPopulation(builder, false);
+ addPopulationDensity(builder, false);
+ builder.setName("Population density");
+ final FeatureProjection projection = builder.project().orElseThrow();
+ assertNotEquals(projection.typeRequested,
projection.typeWithDependencies);
+ assertSetEquals(Set.of("name", "population", "area"),
projection.dependencies());
+ assertPropertyNamesEqual(projection.typeRequested, "country",
"population", "density");
+ assertPropertyNamesEqual(projection.typeWithDependencies, "country",
"population", "density", "area");
+
+ // Property is an operation because we requested the "deferred" mode.
+ assertInstanceOf(Operation.class,
projection.typeWithDependencies.getProperty("density"));
+
+ // Operation should have been replaced by a view because of missing
dependencies.
+ assertInstanceOf(OperationView.class,
projection.typeRequested.getProperty("density"));
+ verifyDensityOnFeatureInstance(projection);
+ }
+
+ /**
+ * Tests the creation of a feature where an operation has been replaced by
a simpler one.
+ * This case happen when a pure-Java operation has been replaced by a
<abbr>SQL</abbr> expression,
+ * in which case the expression is simpler from <abbr>SIS</abbr>
perspective. A consequence of this
+ * simplification is that it may remove the need for some dependencies.
+ */
+ @Test
+ public void testSubsetWithReplacedOperation() {
+ final var builder = new FeatureProjectionBuilder(source, null);
+ addCountry(builder);
+ addPopulation(builder, false);
+ addPopulationDensity(builder, true);
+ builder.setName("Population density");
+ FeatureProjection projection = builder.project().orElseThrow();
+ final Set<String> expected = new HashSet<>(Set.of("country",
"population", "density"));
+ projection = new FeatureProjection(projection, (name, expression) -> {
+ assertTrue(expected.remove(name), name);
+ if (name.equals("density")) {
+ return ff.literal(4.08);
+ }
+ return expression;
+ });
+ assertTrue(expected.isEmpty(), expected.toString());
+ assertSame(projection.typeRequested, projection.typeWithDependencies);
// Because no more extra dependency.
+ assertSetEquals(Set.of("name", "population"),
projection.dependencies());
+ assertPropertyNamesEqual(projection.typeRequested, "country",
"population", "density");
+ verifyDensityOnFeatureInstance(projection);
+ }
+
+ /**
+ * Creates a feature instance, applies the projection and verifies the
result.
+ *
+ * @param projection the projection to apply.
+ */
+ private void verifyDensityOnFeatureInstance(final FeatureProjection
projection) {
+ assertValueClassEquals(String.class, projection, "country");
+ assertValueClassEquals(Integer.class, projection, "population");
+ final Feature result = applyOnFeatureInstance(projection);
+ assertEquals("Canada", result.getPropertyValue("country"));
+ assertEquals(40769890, result.getPropertyValue("population"));
+ assertEquals(4.08, assertInstanceOf(Double.class,
result.getPropertyValue("density")), 1.01);
+ }
+}
diff --git
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAdapter.java
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAdapter.java
index 413665c3cf..ea303cb847 100644
---
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAdapter.java
+++
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureAdapter.java
@@ -343,7 +343,7 @@ final class FeatureAdapter {
*/
final Feature createFeature(final InfoStatements stmts, final ResultSet
result) throws Exception {
final Feature feature = featureType.newInstance();
- for (int i=0; i<attributes.length; i++) {
+ for (int i=0; i < attributes.length; i++) {
final Column column = attributes[i];
final Object value = column.valueGetter.getValue(stmts, result,
i+1);
if (value != null) {
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 cef33f702c..e2c49b3962 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
@@ -465,8 +465,8 @@ final class FeatureStream extends DeferredStream<Feature> {
final JDBCType type = filterToSQL.writeFunction(columnSQL,
expression);
if (type != null) try {
columnSQL.append(" AS ").appendIdentifier(name);
- expression = new ComputedColumn(database, type, name,
- columnSQL.query(connection,
spatialInformation));
+ String sql = columnSQL.query(connection,
spatialInformation);
+ expression = new ComputedColumn(database, type, name,
sql);
} catch (Exception e) {
throw cannotExecute(e);
}
@@ -483,7 +483,7 @@ final class FeatureStream extends DeferredStream<Feature> {
final var unhandled = new BitSet();
final var reusedNames = new HashSet<String>();
projected = new Table(projected, queriedProjection, reusedNames,
unhandled);
- completion =
queriedProjection.afterPreprocessing(unhandled.stream().toArray());
+ completion =
queriedProjection.forPreexistingFeatureInstances(unhandled.stream().toArray());
if (completion != null &&
!reusedNames.containsAll(completion.dependencies())) {
/*
* Cannot use `projected` because some expressions need
properties available only
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 0d52a4c275..9ee1019a4e 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
@@ -629,7 +629,7 @@ public class FeatureQuery extends Query implements
Cloneable, Emptiable, Seriali
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.setPreferredName(alias); // Need to be invoked even
if the alias is null.
item.setValueGetter(expression, type ==
ProjectionType.STORED);
return true;
}
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
index e0f372470a..b4cddd1f8d 100644
---
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
+++
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
@@ -1014,6 +1014,11 @@ public class Vocabulary extends IndexedResourceBundle {
*/
public static final short Plugins = 160;
+ /**
+ * {0} + dependencies
+ */
+ public static final short PlusDependencies_1 = 281;
+
/**
* Preprocessing
*/
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
index 3ae9fe1411..6b870cc509 100644
---
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
+++
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
@@ -207,6 +207,7 @@ Panchromatic = Panchromatic
Parenthesis_2 = {0} ({1})
Paths = Paths
Plugins = Plug-ins
+PlusDependencies_1 = {0} + dependencies
Preprocessing = Preprocessing
Projected = Projected
Property = Property
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
index c1a58d477a..89762168e2 100644
---
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
+++
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -214,6 +214,7 @@ Panchromatic = Panchromatique
Parenthesis_2 = {0} ({1})
Paths = Chemins
Plugins = Modules d\u2019extension
+PlusDependencies_1 = {0} + d\u00e9pendances
Preprocessing = Pr\u00e9traitement
Projected = Projet\u00e9
Property = Propri\u00e9t\u00e9