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 d8bbaf214a Clarify which `Feature` instance is used for the different projection types. Other opportunistic clarifications as we tried to verify the behavior. Replace `ExpressionOperation` by `LinkOperation` when applicable. d8bbaf214a is described below commit d8bbaf214a1c60b20a7928bd11457b13b7061594 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue May 2 19:29:16 2023 +0200 Clarify which `Feature` instance is used for the different projection types. Other opportunistic clarifications as we tried to verify the behavior. Replace `ExpressionOperation` by `LinkOperation` when applicable. --- .../org/apache/sis/feature/AbstractFeature.java | 12 ++- .../java/org/apache/sis/feature/DenseFeature.java | 1 + .../org/apache/sis/feature/EnvelopeOperation.java | 21 +---- .../apache/sis/feature/ExpressionOperation.java | 65 +++++++++++++--- .../org/apache/sis/feature/FeatureOperations.java | 25 +++--- .../org/apache/sis/feature/OperationResult.java | 71 +++++++++++++++++ .../java/org/apache/sis/feature/SparseFeature.java | 1 + .../apache/sis/feature/StringJoinOperation.java | 13 +--- .../java/org/apache/sis/filter/PropertyValue.java | 1 + .../apache/sis/{ => internal}/filter/XPath.java | 8 +- .../test/java/org/apache/sis/filter/XPathTest.java | 1 + .../org/apache/sis/internal/map/SEPortrayer.java | 5 +- .../java/org/apache/sis/storage/FeatureQuery.java | 90 +++++++++++++++++----- .../java/org/apache/sis/storage/FeatureSubset.java | 2 +- .../org/apache/sis/storage/FeatureQueryTest.java | 8 +- 15 files changed, 246 insertions(+), 78 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java index 3d823e76e7..becfe57dfb 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/AbstractFeature.java @@ -67,6 +67,13 @@ import org.opengis.feature.Operation; * <li>the cardinality of all attributes is constrained to [1 … 1].</li> * </ul> * + * <h2>Operations</h2> + * Properties that are instances of {@link Operation} are usually not stored in {@code Feature} instances. + * Instead, the {@link Operation#apply Operation.apply(…)} method is invoked every times that the property + * value is requested. {@code AbstractFeature} does not cache operation results. + * Those results are usually read-only, but may be writable under the conditions documented in + * {@link #setOperationValue(String, Object)}. + * * <h2>Limitations</h2> * <ul> * <li><b>Multi-threading:</b> {@code AbstractFeature} instances are <strong>not</strong> thread-safe. @@ -439,7 +446,10 @@ public abstract class AbstractFeature implements Feature, Serializable { * Executes the parameterless operation of the given name and sets the value of its result. * This method is the complement of {@link #getOperationValue(String)} for subclasses where * some properties may be operations. Not all operations accept assignments, - * but the {@linkplain FeatureOperations#link link} operation for instance does. + * but the {@linkplain FeatureOperations#link link} and + * {@linkplain FeatureOperations#compound compound} operations (for instances) do. + * Whether an operation is writable or not depends on whether the computed {@link Property} + * supports {@link Attribute#setValue(Object)} or {@link FeatureAssociation#setValue(Feature)}. * * @param name the name of the operation to execute. The caller is responsible to ensure that the * property type for that name is an instance of {@link Operation}. diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java b/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java index 319a81f9d1..54193162b0 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/DenseFeature.java @@ -60,6 +60,7 @@ final class DenseFeature extends AbstractFeature implements Cloneable { /** * The properties (attributes or feature associations) in this feature. + * This array does not include operation results, which are always computed on the fly. * * Conceptually, values in this array are {@link Property} instances. However, at first we will store only * the property <em>values</em>, and convert to an array of type {@code Property[]} only when at least one diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java b/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java index 6018a951d6..88594a2fa2 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/EnvelopeOperation.java @@ -272,24 +272,17 @@ final class EnvelopeOperation extends AbstractOperation { * The attributes that contains the result of union of all envelope extracted from other attributes. * Value is calculated each time it is accessed. */ - private final class Result extends AbstractAttribute<Envelope> { + private final class Result extends OperationResult<Envelope> { /** * For cross-version compatibility. */ - private static final long serialVersionUID = 926172863066901618L; - - /** - * The feature specified to the {@link StringJoinOperation#apply(Feature, ParameterValueGroup)} method. - */ - @SuppressWarnings("serial") // Most SIS implementations are serializable. - private final Feature feature; + private static final long serialVersionUID = 4900962888075807964L; /** * Creates a new attribute for the given feature. */ Result(final Feature feature) { - super(resultType); - this.feature = feature; + super(resultType, feature); } /** @@ -406,14 +399,6 @@ final class EnvelopeOperation extends AbstractOperation { throw new FeatureOperationException(Errors.formatInternational(Errors.Keys.CanNotTransformEnvelope), e); } } - - /** - * Unconditionally throws an {@link UnsupportedOperationException}. - */ - @Override - public void setValue(Envelope value) { - throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, Attribute.class)); - } } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java b/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java index c5990072e8..7b897b979a 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java @@ -31,7 +31,6 @@ import org.apache.sis.internal.filter.Visitor; // Branch-dependent imports import org.opengis.feature.Feature; import org.opengis.feature.Property; -import org.opengis.feature.Attribute; import org.opengis.feature.AttributeType; import org.opengis.feature.IdentifiedType; import org.opengis.filter.Filter; @@ -70,7 +69,7 @@ final class ExpressionOperation<V> extends AbstractOperation { * The type of result of evaluating the expression. */ @SuppressWarnings("serial") // Apache SIS implementations are serializable. - private final AttributeType<? super V> result; + private final AttributeType<V> resultType; /** * The name of all feature properties that are known to be read by the expression. @@ -85,17 +84,35 @@ final class ExpressionOperation<V> extends AbstractOperation { * * @param identification the name of the operation, together with optional information. * @param expression the expression to evaluate on feature instances. - * @param result type of values computed by the expression. + * @param resultType type of values computed by the expression. */ - ExpressionOperation(final Map<String,?> identification, - final Function<? super Feature, ? extends V> expression, - final AttributeType<? super V> result) + static <V> AbstractOperation create(final Map<String,?> identification, + final Function<? super Feature, ? extends V> expression, + final AttributeType<? super V> resultType) + { + if (expression instanceof ValueReference<?,?>) { + final String xpath = ((ValueReference<?,?>) expression).getXPath(); + if (xpath.equals(resultType.getName().toString())) { + return new LinkOperation(identification, resultType); + } + } + return new ExpressionOperation<>(identification, expression, resultType); + } + + /** + * Creates a generic operation when no optimized case has been identifier. + */ + private ExpressionOperation(final Map<String,?> identification, + final Function<? super Feature, ? extends V> expression, + final AttributeType<V> resultType) { super(identification); this.expression = expression; - this.result = result; + this.resultType = resultType; if (expression instanceof Expression<?,?>) { - dependencies = DependencyFinder.search((Expression<Feature,?>) expression); + @SuppressWarnings("unchecked") + var c = (Expression<Feature,?>) expression; // Cast is okay because we will not pass or request any `Feature` instance. + dependencies = DependencyFinder.search(c); } else { dependencies = Set.of(); } @@ -114,7 +131,7 @@ final class ExpressionOperation<V> extends AbstractOperation { */ @Override public IdentifiedType getResult() { - return result; + return resultType; } /** @@ -136,9 +153,33 @@ final class ExpressionOperation<V> extends AbstractOperation { */ @Override public Property apply(final Feature feature, ParameterValueGroup parameters) { - final Attribute<? super V> instance = result.newInstance(); - instance.setValue(expression.apply(feature)); - return instance; + return new Result(feature); + } + + /** + * The attributes that delegates computation to the expression. + * Value is calculated each time it is accessed. + */ + private final class Result extends OperationResult<V> { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = -19004252522001532L; + + /** + * Creates a new attribute for the given feature. + */ + Result(final Feature feature) { + super(resultType, feature); + } + + /** + * Delegates the computation to the user-supplied expression. + */ + @Override + public V getValue() { + return expression.apply(feature); + } } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java index 15c36b01cb..db15ddcb83 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java @@ -130,7 +130,7 @@ public final class FeatureOperations extends Static { * Creates an operation which is only an alias for another property. * * <h4>Example</h4> - * features often have a property that can be used as identifier or primary key. + * Features often have a property that can be used as identifier or primary key. * But the name of that property may vary between features of different types. * For example, features of type <b>Country</b> may have identifiers named “ISO country code” * while features of type <b>Car</b> may have identifiers named “license plate number”. @@ -274,22 +274,23 @@ 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 result type of values computed by the expression and assigned to the feature property. + * @param resultType type of values computed by the expression and assigned to the feature property. * @return a feature operation which computes its values using the given expression. * * @since 1.4 */ - public static <V> Operation expression(final Map<String,?> identification, - final Function<? super Feature, ? extends V> expression, - final AttributeType<? super V> result) + public static <V> Operation function(final Map<String,?> identification, + final Function<? super Feature, ? extends V> expression, + final AttributeType<? super V> resultType) { ArgumentChecks.ensureNonNull("expression", expression); - return POOL.unique(new ExpressionOperation<>(identification, expression, result)); + ArgumentChecks.ensureNonNull("result", resultType); + return POOL.unique(ExpressionOperation.create(identification, expression, resultType)); } /** * Creates an operation which delegates the computation to a given expression producing values of unknown type. - * This method can be used as an alternative to {@link #expression expression(…)} when the constraint on the + * This method can be used as an alternative to {@link #function function(…)} when the constraint on the * parameterized type {@code <V>} between {@code expression} and {@code result} cannot be enforced at compile time. * This method casts or converts the expression to the expected type by a call to * {@link Expression#toValueType(Class)}. @@ -297,16 +298,16 @@ 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 result type of values computed by the expression and assigned to the feature property. + * @param resultType type of values computed by the expression and assigned to the feature property. * @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. * * @since 1.4 */ - public static <V> Operation expressionToResult(final Map<String,?> identification, - final Expression<? super Feature, ?> expression, - final AttributeType<V> result) + public static <V> Operation expression(final Map<String,?> identification, + final Expression<? super Feature, ?> expression, + final AttributeType<V> resultType) { - return expression(identification, expression.toValueType(result.getValueClass()), result); + return function(identification, expression.toValueType(resultType.getValueClass()), resultType); } } diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/OperationResult.java b/core/sis-feature/src/main/java/org/apache/sis/feature/OperationResult.java new file mode 100644 index 0000000000..b05f884c54 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/OperationResult.java @@ -0,0 +1,71 @@ +/* + * 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; + +import org.apache.sis.util.resources.Errors; + +// Branch-dependent imports +import org.opengis.feature.Feature; +import org.opengis.feature.Attribute; +import org.opengis.feature.AttributeType; + + +/** + * Base class of attributes that are the result of a feature operation. + * This base class is defined for making easier to identify where computations are done. + * + * @todo A future version may provide caching services, methods for taking a snapshot, <i>etc.</i> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.4 + * + * @param <V> the type of attribute values. + * + * @since 1.4 + */ +abstract class OperationResult<V> extends AbstractAttribute<V> { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = 1418917854672134381L; + + /** + * The feature instance to use as a source for computing the result. + */ + @SuppressWarnings("serial") // Most SIS implementations are serializable. + protected final Feature feature; + + /** + * Creates a new operation for a result of the given type. + * + * @param type information about the attribute (base Java class, domain of values, <i>etc.</i>). + */ + protected OperationResult(final AttributeType<V> type, final Feature feature) { + super(type); + this.feature = feature; + } + + /** + * Retro-propagate an operation result to the properties in the source feature instance. + * This is an optional operation. + * The default implementation unconditionally throws an {@link UnsupportedOperationException}. + */ + @Override + public void setValue(V value) { + throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, Attribute.class)); + } +} diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java b/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java index 55af5252ed..7b3ddce242 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/SparseFeature.java @@ -82,6 +82,7 @@ final class SparseFeature extends AbstractFeature implements Cloneable { /** * The properties (attributes or feature associations) in this feature. + * This map does not include operation results, which are always computed on the fly. * * Conceptually, values in this map are {@link Property} instances. However, at first we will store * only the property <em>values</em>, and build the full {@code Property} objects only if they are diff --git a/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java b/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java index a618de04a4..3bc72ab684 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java +++ b/core/sis-feature/src/main/java/org/apache/sis/feature/StringJoinOperation.java @@ -317,24 +317,17 @@ final class StringJoinOperation extends AbstractOperation { * The attributes that contains the result of concatenating the string representation of other attributes. * Value is calculated each time it is accessed. */ - private final class Result extends AbstractAttribute<String> { + private final class Result extends OperationResult<String> { /** * For cross-version compatibility. */ - private static final long serialVersionUID = -8435975199763452547L; - - /** - * The feature specified to the {@link StringJoinOperation#apply(Feature, ParameterValueGroup)} method. - */ - @SuppressWarnings("serial") // Most SIS implementations are serializable. - private final Feature feature; + private static final long serialVersionUID = -555025854115540108L; /** * Creates a new attribute for the given feature. */ Result(final Feature feature) { - super(resultType); - this.feature = feature; + super(resultType, feature); } /** diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java index cae63556d3..4fb0bcbe1a 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java +++ b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java @@ -26,6 +26,7 @@ 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.internal.filter.XPath; import org.apache.sis.util.resources.Errors; // Branch-dependent imports diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/XPath.java b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/XPath.java similarity index 90% rename from core/sis-feature/src/main/java/org/apache/sis/filter/XPath.java rename to core/sis-feature/src/main/java/org/apache/sis/internal/filter/XPath.java index c1ca1cccb6..aa40ecceba 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/filter/XPath.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/XPath.java @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.sis.filter; +package org.apache.sis.internal.filter; import java.util.List; import java.util.ArrayList; @@ -32,9 +32,11 @@ import static org.apache.sis.util.CharSequences.*; * @version 1.3 * @since 0.4 */ -final class XPath extends Static { +public final class XPath extends Static { /** * The separator between path components. + * Should not be used for URL or Unix name separator, even if the character is the same. + * We use this constant for identifying locations in the code where there is some X-Path parsing. */ public static final char SEPARATOR = '/'; @@ -57,7 +59,7 @@ final class XPath extends Static { * if there is no separator. If non-null, the list always contains at least one element. * @throws IllegalArgumentException if the XPath contains at least one empty component. */ - static List<String> split(final String xpath) { + public static List<String> split(final String xpath) { int next = xpath.indexOf(SEPARATOR); if (next < 0) { return null; diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java index 2d7e4e599c..a8f11aa454 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/filter/XPathTest.java @@ -16,6 +16,7 @@ */ package org.apache.sis.filter; +import org.apache.sis.internal.filter.XPath; import org.apache.sis.test.TestCase; import org.junit.Test; diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java index 61da8fd80e..6eff0d15a4 100644 --- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java +++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/SEPortrayer.java @@ -42,6 +42,7 @@ import org.apache.sis.filter.DefaultFilterFactory; import org.apache.sis.geometry.Envelopes; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.internal.feature.AttributeConvention; +import org.apache.sis.internal.filter.XPath; import org.apache.sis.storage.FeatureQuery; import org.apache.sis.portrayal.MapItem; import org.apache.sis.portrayal.MapLayer; @@ -710,7 +711,7 @@ public final class SEPortrayer { * Removes any xpath elements, keep only the root property name. */ private static String stripXpath(String attName) { - int index = attName.indexOf('/'); + int index = attName.indexOf(XPath.SEPARATOR); if (index == 0) { attName = attName.substring(1); // Remove first slash final Pattern pattern = Pattern.compile("(\\{[^\\{\\}]*\\})|(\\[[^\\[\\]]*\\])|/{1}"); @@ -722,7 +723,7 @@ matches: while (matcher.find()) { sb.append(attName.substring(position, matcher.start())); position = matcher.end(); switch (match.charAt(0)) { - case '/': { + case XPath.SEPARATOR: { // We do not query precisely sub elements. position = attName.length(); break matches; diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java index 0f4bb9e3e7..46947ce71e 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java @@ -37,10 +37,12 @@ import org.apache.sis.feature.builder.AttributeTypeBuilder; import org.apache.sis.internal.feature.AttributeConvention; import org.apache.sis.internal.feature.FeatureExpression; import org.apache.sis.internal.filter.SortByComparator; +import org.apache.sis.internal.filter.XPath; import org.apache.sis.internal.storage.Resources; import org.apache.sis.filter.DefaultFilterFactory; import org.apache.sis.filter.Optimization; import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.ArraysExt; import org.apache.sis.util.CharSequences; import org.apache.sis.util.collection.Containers; import org.apache.sis.util.iso.Names; @@ -254,6 +256,25 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { return (projection != null) ? projection.clone() : null; } + /** + * Returns the properties to be stored in the target feature. + */ + final NamedExpression[] getStoredProjection() { + final NamedExpression[] stored = getProjection(); + if (stored != null) { + int count = 0; + for (final NamedExpression p : stored) { + if (p.type == ProjectionType.STORED) { + stored[count++] = p; + } + } + if (count != 0) { + return ArraysExt.resize(stored, count); + } + } + return null; + } + /** * Sets the approximate area of feature instances to include in the subset. * This convenience method creates a filter that checks if the bounding box @@ -430,9 +451,33 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { * and the result is stored as a feature attribute. * The feature property type will be {@link Attribute} and its value will be modifiable. * This is the default projection type. + * + * <h4>Feature instances in expression evaluation</h4> + * The features given in calls to {@link Expression#apply(Object)} are instances from the + * <em>source</em> {@link FeatureSet}, before filtering. */ STORED, + /* + * The expression is evaluated every times that the property value is requested. + * This projection type is similar to {@link #COMPLETING}, except that the features + * given in calls to {@link Expression#apply(Object)} are the same instances than + * the ones used by {@link #STORED}. + * + * <div class="note"><b>Note on naming:</b> + * the {@code STORED} and {@code VIRTUAL} enumeration values are named according usage in SQL + * {@code GENERATE ALWAYS} statement. Those two keywords work on columns in the source tables. + * </div> + * + * <h4>Feature instances in expression evaluation</h4> + * The combination of deferred calculation (like {@link #COMPLETING}) and usage of feature instances + * from the <em>source</em> {@link FeatureSet} (like {@link #STORED}) may cause this projection type + * to retain the source feature instances for a longer time than other types. + * + * @todo Waiting to see if there is a need for this type before to implement it. + */ + // VIRTUAL, + /** * The expression is evaluated every times that the property value is requested. * The feature property type will be {@link Operation}. @@ -445,14 +490,19 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { * <li>Computation result should not be stored in order to reduce memory usage.</li> * </ul> * + * <h4>Feature instances in expression evaluation</h4> + * The features given in calls to {@link Expression#apply(Object)} are instances from the <em>target</em> + * {@link FeatureSet}, after filtering. The instances from the source {@code FeatureSet} are no longer + * available when the expression is executed. Consequently, all fields that are necessary for computing + * a {@code COMPLETING} field shall have been first copied in {@link #STORED} fields. + * + * <div class="note"><b>Note on naming:</b> + * verb tense <i>-ing</i> instead of <i>-ed</i> is for emphasizing that the data used for computation + * are current (filtered) data instead of past (original) data.</div> + * * @see FeatureOperations#expression(Map, Function, AttributeType) */ - VIRTUAL - - /* - * Examples of other enumeration values that we may add in the future: - * GENERATED for meaning "STORED but read-only", CACHED for lazy computation. - */ + COMPLETING } /** @@ -586,10 +636,9 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { */ @Override public String toString() { - final StringBuilder buffer = new StringBuilder(); - buffer.append(getClass().getSimpleName()).append('['); // Class name without enclosing class. + final StringBuilder buffer = new StringBuilder("SELECT "); appendTo(buffer); - return buffer.append(']').toString(); + return buffer.toString(); } /** @@ -597,11 +646,14 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { */ final void appendTo(final StringBuilder buffer) { if (expression instanceof Literal<?,?>) { - buffer.append('“').append(((Literal<?,?>) expression).getValue()).append('”'); + buffer.append('‘').append(((Literal<?,?>) expression).getValue()).append('’'); } else if (expression instanceof ValueReference<?,?>) { - buffer.append(((ValueReference<?,?>) expression).getXPath()); + buffer.append('“').append(((ValueReference<?,?>) expression).getXPath()).append('”'); } else { - buffer.append(expression.getFunctionName()); + buffer.append("=“").append(expression.getFunctionName()).append("”()"); + } + if (type != ProjectionType.STORED) { + buffer.append(' ').append(type); } if (alias != null) { buffer.append(" AS “").append(alias).append('”'); @@ -721,7 +773,7 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { continue; } String xpath = ((ValueReference<?,?>) expression).getXPath().trim(); - xpath = xpath.substring(xpath.lastIndexOf('/') + 1); // Works also if '/' is not found. + xpath = xpath.substring(xpath.lastIndexOf(XPath.SEPARATOR) + 1); // Works also if '/' is not found. if (!(xpath.isEmpty() || names.contains(xpath))) { text = xpath; } @@ -737,18 +789,20 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { } while (!names.add(text.toString())); name = Names.createLocalName(null, null, text); } - resultType.setName(name); /* - * If the attribute that we just added should be virtual, - * replace the attribute by an operation. + * If the attribute that we just added should be virtual, replace the attribute by an operation. + * We need to keep the property name computed by `fex.expectedType(…)` for the operation result, + * because that name is the name of the link to create if the operation is `ValueReference`. */ - if (item.type == ProjectionType.VIRTUAL && resultType instanceof AttributeTypeBuilder<?>) { + if (item.type == ProjectionType.COMPLETING && resultType instanceof AttributeTypeBuilder<?>) { final var ab = (AttributeTypeBuilder<?>) resultType; final AttributeType<?> storedType = ab.build(); if (ftb.properties().remove(resultType)) { final var properties = Map.of(AbstractOperation.NAME_KEY, name); - ftb.addProperty(FeatureOperations.expressionToResult(properties, expression, storedType)); + ftb.addProperty(FeatureOperations.expression(properties, expression, storedType)); } + } else { + resultType.setName(name); } } return ftb.build(); diff --git a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java index dc4a9f6992..5b2e67c900 100644 --- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java +++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java @@ -139,7 +139,7 @@ final class FeatureSubset extends AbstractFeatureSet { * Transform feature instances. * Note: "projection" here is in relational database sense, not map projection. */ - final FeatureQuery.NamedExpression[] projection = query.getProjection(); + final FeatureQuery.NamedExpression[] projection = query.getStoredProjection(); if (projection != null) { @SuppressWarnings({"unchecked", "rawtypes"}) final Expression<? super Feature,?>[] expressions = new Expression[projection.length]; diff --git a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java index b4aa6f3786..fb70453eb6 100644 --- a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java +++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Arrays; import java.util.Iterator; import java.util.stream.Collectors; +import org.apache.sis.feature.Features; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.internal.storage.MemoryFeatureSet; import org.apache.sis.filter.DefaultFilterFactory; @@ -328,7 +329,7 @@ public final class FeatureQueryTest extends TestCase { * Shortcut for creating expression for a projection computed on-the-fly. */ private static FeatureQuery.NamedExpression virtualProjection(final Expression<Feature, ?> expression, final String alias) { - return new FeatureQuery.NamedExpression(expression, Names.createLocalName(null, null, alias), FeatureQuery.ProjectionType.VIRTUAL); + return new FeatureQuery.NamedExpression(expression, Names.createLocalName(null, null, alias), FeatureQuery.ProjectionType.COMPLETING); } /** @@ -367,6 +368,11 @@ public final class FeatureQueryTest extends TestCase { assertEquals(3, instance.getPropertyValue("value1")); assertEquals(3, instance.getPropertyValue("renamed1")); assertEquals("a literal", instance.getPropertyValue("computed")); + + // The `ValueReference` operation should have been optimized as a link. + assertEquals("value1", Features.getLinkTarget(pt2).get()); + assertTrue(Features.getLinkTarget(pt1).isEmpty()); + assertTrue(Features.getLinkTarget(pt3).isEmpty()); } /**