You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2025/05/21 16:17:38 UTC
(sis) 01/01: Merge branch 'geoapi-3.1'. Contains improvements in: `SQLStore`, ShapeFile (incubator), conversions from AWT shapes to JTS, partial DuckDB support, and use of HTTP ranges in GeoHEIF.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sis.git
commit 3d377745a9cbb746e07f482fe71082dcfa657854
Merge: ac4ad3f36a ffa96dbde4
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed May 21 18:14:30 2025 +0200
Merge branch 'geoapi-3.1'.
Contains improvements in: `SQLStore`, ShapeFile (incubator), conversions from AWT shapes to JTS, partial DuckDB support, and use of HTTP ranges in GeoHEIF.
.../apache/sis/cloud/aws/s3/ClientFileSystem.java | 2 +-
.../org.apache.sis.feature/main/module-info.java | 1 +
.../org/apache/sis/feature/AbstractOperation.java | 4 +-
.../apache/sis/feature/ExpressionOperation.java | 2 +-
.../org/apache/sis/feature/FeatureOperations.java | 77 ++-
.../main/org/apache/sis/feature/Features.java | 25 +-
.../apache/sis/feature/StringJoinOperation.java | 14 +
.../sis/feature/builder/AttributeTypeBuilder.java | 14 +-
.../sis/feature/builder/FeatureTypeBuilder.java | 10 +-
.../sis/feature/builder/OperationWrapper.java | 30 +-
.../sis/feature/builder/PropertyTypeBuilder.java | 32 +-
.../apache/sis/feature/builder/TypeBuilder.java | 2 +-
.../org/apache/sis/feature/internal/Resources.java | 5 +
.../sis/feature/internal/Resources.properties | 1 +
.../sis/feature/internal/Resources_fr.properties | 1 +
.../main/org/apache/sis/feature/package-info.java | 2 +-
.../sis/feature/privy/FeatureExpression.java | 83 ++-
.../sis/feature/privy/FeatureProjection.java | 341 ++++++++++
.../feature/privy/FeatureProjectionBuilder.java | 739 +++++++++++++++++++++
.../org/apache/sis/feature/privy/FeatureView.java | 129 ++++
.../org/apache/sis/filter/ArithmeticFunction.java | 8 +-
.../org/apache/sis/filter/AssociationValue.java | 22 +-
.../org/apache/sis/filter/ConvertFunction.java | 21 +-
.../apache/sis/filter/DefaultFilterFactory.java | 26 +-
.../apache/sis/filter/InvalidXPathException.java | 78 +++
.../main/org/apache/sis/filter/LeafExpression.java | 15 +-
.../main/org/apache/sis/filter/PropertyValue.java | 72 +-
.../main/org/apache/sis/filter/privy/XPath.java | 18 +-
.../apache/sis/filter/sqlmm/FunctionWithSRID.java | 24 +-
.../apache/sis/filter/sqlmm/SpatialFunction.java | 36 +-
.../sis/geometry/wrapper/jts/ConverterTo2D.java | 227 +++++++
.../sis/geometry/wrapper/jts/ShapeConverter.java | 73 +-
.../feature/builder/FeatureTypeBuilderTest.java | 29 +
.../geometry/wrapper/jts/ShapeConverterTest.java | 40 +-
.../org/apache/sis/metadata/sql/privy/Dialect.java | 65 +-
.../apache/sis/metadata/sql/privy/Reflection.java | 2 +
.../apache/sis/metadata/sql/privy/Supports.java | 15 +
.../main/org/apache/sis/storage/landsat/Band.java | 27 +-
.../org/apache/sis/storage/landsat/BandGroup.java | 15 +
.../apache/sis/storage/landsat/LandsatStore.java | 27 +-
.../org/apache/sis/storage/sql/duckdb/DuckDB.java | 2 +-
.../sis/storage/sql/duckdb/package-info.java | 4 +
.../apache/sis/storage/sql/feature/Analyzer.java | 7 +-
.../org/apache/sis/storage/sql/feature/Column.java | 50 +-
.../apache/sis/storage/sql/feature/Database.java | 56 +-
.../sis/storage/sql/feature/FeatureAdapter.java | 20 +-
.../sis/storage/sql/feature/FeatureIterator.java | 31 +-
.../sis/storage/sql/feature/FeatureStream.java | 38 +-
.../sis/storage/sql/feature/GeometryEncoding.java | 93 ++-
.../storage/sql/feature/GeometryTypeEncoding.java | 2 +
.../sis/storage/sql/feature/InfoStatements.java | 124 +++-
.../sis/storage/sql/feature/QueryAnalyzer.java | 7 +-
.../sis/storage/sql/feature/SelectionClause.java | 10 +-
.../storage/sql/feature/SelectionClauseWriter.java | 10 +-
.../sis/storage/sql/feature/SpatialSchema.java | 9 +-
.../org/apache/sis/storage/sql/feature/Table.java | 48 +-
.../sis/storage/sql/feature/TableAnalyzer.java | 10 +
.../sis/storage/sql/feature/ValueGetter.java | 2 +-
.../apache/sis/storage/sql/postgis/Postgres.java | 4 +-
.../org/apache/sis/util/stream/DeferredStream.java | 10 +-
.../org/apache/sis/storage/sql/SQLStoreTest.java | 30 +-
.../org/apache/sis/io/stream/ChannelDataInput.java | 15 +
.../org/apache/sis/io/stream/HttpByteChannel.java | 21 +
.../main/org/apache/sis/storage/FeatureQuery.java | 58 +-
.../main/org/apache/sis/storage/FeatureSubset.java | 6 +-
.../apache/sis/storage/base/FeatureProjection.java | 381 -----------
.../sis/storage/base/TiledDeferredImage.java | 1 +
.../apache/sis/storage/base/TiledGridCoverage.java | 19 +-
.../org/apache/sis/storage/internal/Resources.java | 5 +
.../sis/storage/internal/Resources.properties | 1 +
.../sis/storage/internal/Resources_fr.properties | 1 +
.../main/org/apache/sis/storage/tiling/Tile.java | 19 +
.../sis/util/privy/UnmodifiableArrayList.java | 3 +-
.../apache/sis/storage/geoheif/FromImageIO.java | 65 +-
.../main/org/apache/sis/storage/geoheif/Image.java | 43 +-
.../apache/sis/storage/geoheif/ImageResource.java | 152 +++--
.../sis/storage/geoheif/ResourceBuilder.java | 6 +-
.../sis/storage/geoheif/UncompressedImage.java | 69 +-
.../org/apache/sis/storage/isobmff/ByteRanges.java | 163 +++++
.../org/apache/sis/storage/isobmff/ByteReader.java | 92 ---
.../org/apache/sis/storage/isobmff/Reader.java | 14 +-
.../apache/sis/storage/isobmff/base/ItemData.java | 32 +-
.../sis/storage/isobmff/base/ItemLocation.java | 96 +--
.../sis/storage/shapefile/ShapefileStore.java | 12 +-
84 files changed, 3099 insertions(+), 1096 deletions(-)
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
index 1ed310c507,5d27304400..0449cc2a29
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/ExpressionOperation.java
@@@ -57,7 -62,7 +57,7 @@@ final class ExpressionOperation<V> exte
* The expression to which to delegate the execution of this operation.
*/
@SuppressWarnings("serial") // Not statically typed as serializable.
- private final Function<? super AbstractFeature, ? extends V> expression;
- final Function<? super Feature, ? extends V> expression;
++ final Function<? super AbstractFeature, ? extends V> expression;
/**
* The type of result of evaluating the expression.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
index 2dc4406318,05210b25e4..4df8b93098
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/FeatureOperations.java
@@@ -27,10 -27,17 +27,12 @@@ import org.apache.sis.util.Static
import org.apache.sis.util.collection.WeakHashSet;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.privy.Strings;
+ import org.apache.sis.filter.DefaultFilterFactory;
+ import org.apache.sis.filter.privy.XPath;
import org.apache.sis.setup.GeometryLibrary;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.Feature;
-import org.opengis.feature.Operation;
-import org.opengis.feature.PropertyType;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.FeatureAssociationRole;
-import org.opengis.filter.Expression;
+// Specific to the main branch:
+import org.apache.sis.filter.Expression;
/**
@@@ -158,9 -152,9 +151,9 @@@ public final class FeatureOperations ex
* @param referent the referenced attribute or feature association.
* @return an operation which is an alias for the {@code referent} property.
*
-- * @see Features#getLinkTarget(PropertyType)
++ * @see Features#getLinkTarget(AbstractIdentifiedType)
*/
- public static Operation link(final Map<String,?> identification, final PropertyType referent) {
+ public static AbstractOperation link(final Map<String,?> identification, final AbstractIdentifiedType referent) {
ArgumentChecks.ensureNonNull("referent", referent);
return POOL.unique(new LinkOperation(identification, referent));
}
@@@ -362,4 -348,44 +355,44 @@@
{
return function(identification, expression.toValueType(resultType.getValueClass()), resultType);
}
+
+ /**
+ * Returns an expression for fetching the values of properties identified by the given type.
+ * The returned expression will be the first of the following choices which is applicable:
+ *
+ * <ul>
+ * <li>If the property is an expression built by {@link #expression expression(…)}, then the
+ * expression given to that method, or a derivative of that expression, is returned.</li>
+ * <li>If the property {@linkplain Features#getLinkTarget is a link},
+ * then a {@code ValueReference} fetching the link target is returned.</li>
+ * <li>Otherwise, a {@linkplain DefaultFilterFactory.Features#property value reference}
+ * is created for the name of the given property.</li>
+ * </ul>
+ *
+ * @param property the property for which to get an expression.
+ * @return an expression for fetching the values of the property identified by the given type.
+ * @since 1.5
+ */
- public static Expression<? super Feature, ?> expressionOf(final PropertyType property) {
++ public static Expression<? super AbstractFeature, ?> expressionOf(final AbstractIdentifiedType property) {
+ // Test final class first because it is fast.
+ if (property instanceof ExpressionOperation<?>) {
- final Function<? super Feature, ?> expression = ((ExpressionOperation<?>) property).expression;
++ final Function<? super AbstractFeature, ?> expression = ((ExpressionOperation<?>) property).expression;
+ if (expression instanceof Expression<?,?>) {
- return (Expression<? super Feature, ?>) expression;
++ return (Expression<? super AbstractFeature, ?>) expression;
+ }
+ }
+ String name;
+ final Class<?> type;
- if (property instanceof AttributeType<?>) {
- type = ((AttributeType<?>) property).getValueClass();
++ if (property instanceof DefaultAttributeType<?>) {
++ type = ((DefaultAttributeType<?>) property).getValueClass();
+ name = null;
+ } else {
+ type = Object.class;
+ name = Features.getLinkTarget(property).orElse(null);
+ }
+ if (name == null) {
+ name = property.getName().toString();
+ }
+ return DefaultFilterFactory.forFeatures().property(XPath.fromPropertyName(name), type);
+ }
}
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
index ea862f408f,00ad38f9d1..90d7b2d5e4
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java
@@@ -260,6 -302,28 +261,28 @@@ public final class Features extends Sta
return Optional.empty();
}
+ /**
+ * If the given property is a link or a compound key, returns the name of the referenced properties.
- * This method is similar to {@link #getLinkTarget(PropertyType)}, except that it recognizes also
++ * This method is similar to {@link #getLinkTarget(AbstractIdentifiedType)}, except that it recognizes also
+ * the operations created by {@link FeatureOperations#compound FeatureOperations.compound(…)}.
+ *
+ * @param property the property to test, or {@code null} if none.
+ * @return the referenced property names if {@code property} is a link or a compound key,
+ * or an empty list otherwise.
+ *
- * @see FeatureOperations#compound(Map, String, String, String, PropertyType...)
++ * @see FeatureOperations#compound(Map, String, String, String, AbstractIdentifiedType...)
+ *
+ * @since 1.5
+ */
- public static List<String> getLinkTargets(final PropertyType property) {
++ public static List<String> getLinkTargets(final AbstractIdentifiedType property) {
+ return getLinkTarget(property).map(List::of).orElseGet(() -> {
+ if (property instanceof StringJoinOperation) {
+ return ((StringJoinOperation) property).getAttributeNames();
+ }
+ return List.of();
+ });
+ }
+
/**
* Ensures that all characteristics and property values in the given feature are valid.
* An attribute is valid if it contains a number of values between the
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
index 75bcc06d7f,703c02faac..bb3d2431a0
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/StringJoinOperation.java
@@@ -35,8 -36,21 +36,9 @@@ import org.apache.sis.util.privy.Collec
import org.apache.sis.converter.SurjectiveConverter;
import org.apache.sis.feature.privy.AttributeConvention;
import org.apache.sis.feature.internal.Resources;
+ import org.apache.sis.util.privy.UnmodifiableArrayList;
import org.apache.sis.util.resources.Errors;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.FeatureAssociationRole;
-import org.opengis.feature.IdentifiedType;
-import org.opengis.feature.InvalidPropertyValueException;
-import org.opengis.feature.Operation;
-import org.opengis.feature.Property;
-import org.opengis.feature.PropertyType;
-import org.opengis.feature.PropertyNotFoundException;
-
/**
* An operation concatenating the string representations of the values of multiple properties.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/PropertyTypeBuilder.java
index 7ce9c5ea4e,2c9405313e..d062c56da1
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/PropertyTypeBuilder.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/builder/PropertyTypeBuilder.java
@@@ -18,9 -18,13 +18,10 @@@ package org.apache.sis.feature.builder
import org.opengis.util.GenericName;
import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.feature.internal.Resources;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyType;
-import org.opengis.feature.FeatureAssociationRole;
+// Specific to the main branch:
+import org.apache.sis.feature.AbstractIdentifiedType;
/**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
index 9c1b95725a,03dbf4913a..cdf5e57109
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureExpression.java
@@@ -18,18 -18,20 +18,15 @@@ package org.apache.sis.feature.privy
import java.util.Set;
import org.apache.sis.math.FunctionProperty;
+ import org.apache.sis.util.UnconvertibleObjectException;
import org.apache.sis.filter.Optimization;
import org.apache.sis.filter.DefaultFilterFactory;
- import org.apache.sis.feature.builder.FeatureTypeBuilder;
- import org.apache.sis.feature.builder.PropertyTypeBuilder;
- import org.apache.sis.feature.builder.AttributeTypeBuilder;
import org.apache.sis.filter.internal.Node;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.Feature;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.PropertyType;
-import org.opengis.feature.PropertyNotFoundException;
-import org.opengis.filter.InvalidFilterValueException;
-import org.opengis.filter.Literal;
-import org.opengis.filter.Expression;
-import org.opengis.filter.ValueReference;
+// Specific to the main branch:
+import org.apache.sis.filter.Expression;
- import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.pending.geoapi.filter.Literal;
+import org.apache.sis.pending.geoapi.filter.ValueReference;
/**
@@@ -72,21 -74,38 +69,38 @@@ public interface FeatureExpression<R,V
}
/**
- * Provides the expected type of values produced by this expression when a feature of the given
- * type is evaluated. The resulting type shall describe a "static" property, i.e. it can be a
- * {@link org.apache.sis.feature.DefaultAttributeType} or a {@link org.apache.sis.feature.DefaultAssociationRole}
- * but not an {@link org.apache.sis.feature.AbstractOperation}.
+ * Provides the expected type of values produced by this expression when a feature of a given type is evaluated.
+ * Except for the special case of links (described below), the resulting type shall describe a "static" property,
- * <i>i.e.</i> the type should be an {@link AttributeType} or a {@link org.opengis.feature.FeatureAssociationRole}
- * but not an {@link org.opengis.feature.Operation}. The value of the static property will be set to the result of
++ * <i>i.e.</i> the type should be an {@code AttributeType} or a {@code FeatureAssociationRole}
++ * but not an {@code Operation}. The value of the static property will be set to the result of
+ * evaluating the expression when instances of the {@code FeatureType} will be created.
- * This evaluation will be performed by {@link FeatureProjection#apply(Feature)}.
++ * This evaluation will be performed by {@link FeatureProjection#apply(AbstractFeature)}.
*
- * <p>If this method returns an instance of {@link AttributeTypeBuilder}, then its parameterized
- * type should be the same {@code <V>} than this {@code FeatureExpression}.</p>
+ * <h4>Implementation guideline</h4>
+ * Implementations should declare the property by invoking some of the following methods:
*
- * @param valueType the type of features to be evaluated by the given expression.
- * @param addTo where to add the type of properties evaluated by this expression.
- * @return builder of the added property, or {@code null} if this method cannot add a property.
- * @throws IllegalArgumentException if this method can operate only on some feature types
- * and the given type is not one of them.
+ * <ul>
+ * <li>{@link FeatureProjectionBuilder#source()} for the source of the {@link PropertyType} in next point.</li>
- * <li>{@link FeatureProjectionBuilder#addSourceProperty(PropertyType, boolean)}</li>
++ * <li>{@link FeatureProjectionBuilder#addSourceProperty(AbstractIdentifiedType, boolean)}</li>
+ * <li>{@link FeatureProjectionBuilder#addComputedProperty(PropertyTypeBuilder, boolean)}</li>
+ * </ul>
+ *
+ * Inherited methods such as {@link FeatureProjectionBuilder#addAttribute(Class)} can also be invoked,
+ * but callers will be responsible for providing the value of the properties added by those methods.
- * These values will not be provided by {@link FeatureProjection#apply(Feature)}.
++ * These values will not be provided by {@link FeatureProjection#apply(AbstractFeature)}.
+ *
+ * <h4>Operations</h4>
+ * If the property is a link to another property, such as {@code "sis:identifier"} or {@code "sis:geometry"},
+ * then adding this property may require the addition of dependencies. These dependencies will be detected by
+ * {@link FeatureProjectionBuilder}, which may generate an intermediate {@code FeatureType}.
+ *
+ * @param addTo where to add the type of the property evaluated by this expression.
+ * @return handler of the added property, or {@code null} if the property cannot be added.
- * @throws InvalidFilterValueException if this expression is invalid for the requested operation.
- * @throws PropertyNotFoundException if the property was not found in {@code addTo.source()}.
++ * @throws IllegalArgumentException if this expression is invalid for the requested operation.
++ * @throws IllegalArgumentException if the property was not found in {@code addTo.source()}.
+ * @throws UnconvertibleObjectException if the property default value cannot be converted to the expected type.
*/
- PropertyTypeBuilder expectedType(DefaultFeatureType valueType, FeatureTypeBuilder addTo);
+ FeatureProjectionBuilder.Item expectedType(FeatureProjectionBuilder addTo);
/**
* Tries to cast or convert the given expression to a {@link FeatureExpression}.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjection.java
index 0000000000,b263c1e4dc..9796665544
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjection.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjection.java
@@@ -1,0 -1,341 +1,341 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ package org.apache.sis.feature.privy;
+
+ import java.util.List;
+ import java.util.Set;
+ import java.util.Map;
+ import java.util.LinkedHashMap;
+ import java.util.Optional;
+ import java.util.function.UnaryOperator;
+ import org.apache.sis.util.Debug;
+ import org.apache.sis.util.ArraysExt;
+ import org.apache.sis.util.resources.Vocabulary;
+ import org.apache.sis.util.privy.UnmodifiableArrayList;
+ import org.apache.sis.filter.privy.ListingPropertyVisitor;
+ import org.apache.sis.io.TableAppender;
+
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.filter.Expression;
-import org.opengis.filter.ValueReference;
++// Specific to the main branch:
++import org.apache.sis.filter.Expression;
++import org.apache.sis.feature.AbstractFeature;
++import org.apache.sis.feature.DefaultFeatureType;
++import org.apache.sis.pending.geoapi.filter.ValueReference;
+
+
+ /**
+ * A function applying projections (with "projection" in the <abbr>SQL</abbr> database sense) of features.
+ * Given full feature instances in input, the function returns feature instances containing only a subset
+ * of the properties. The property values may also be different if they are computed by the expressions.
+ *
+ * @author Guilhem Legal (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
+ */
-public final class FeatureProjection implements UnaryOperator<Feature> {
++public final class FeatureProjection implements UnaryOperator<AbstractFeature> {
+ /**
+ * The type of features with the properties explicitly requested by the user.
+ * The property names may differ from the properties of the {@link FeatureProjectionBuilder#source() source}
+ * features if aliases were specified by calls to {@link FeatureProjectionBuilder.Item#setName(GenericName)}.
+ */
- public final FeatureType typeRequested;
++ public final DefaultFeatureType typeRequested;
+
+ /**
+ * The requested type augmented with dependencies required for the execution of operations such as links.
+ * If there is no need for additional properties, then this value is the same as {@link #typeRequested}.
+ * The property names are the same as {@link #typeRequested} (i.e., may be aliases).
+ */
- public final FeatureType typeWithDependencies;
++ public final DefaultFeatureType typeWithDependencies;
+
+ /**
+ * Names of the properties to be stored in the feature instances created by this {@code FeatureProjection}.
+ * Properties that are computed on-the-fly from other properties are not included in this array. Properties
+ * that are present in {@link #typeRequested} but not in the source features are not in this array neither.
+ */
+ private final String[] propertiesToCopy;
+
+ /**
+ * Expressions to apply on the source feature for fetching the property values of the projected feature.
+ * This array has the same length as {@link #propertiesToCopy} and each expression is associated to the
+ * property at the same index.
+ */
- private final Expression<? super Feature, ?>[] expressions;
++ private final Expression<? super AbstractFeature, ?>[] expressions;
+
+ /**
- * Whether the {@link #apply(Feature)} method shall create instances of {@link #typeWithDependencies}.
- * If {@code false}, then the instances given to the {@link #apply(Feature)} method will be assumed
++ * Whether the {@link #apply(AbstractFeature)} method shall create instances of {@link #typeWithDependencies}.
++ * If {@code false}, then the instances given to the {@link #apply(AbstractFeature)} method will be assumed
+ * to be already instances of {@link #typeWithDependencies} and will be modified in-place.
+ */
+ private final boolean createInstance;
+
+ /**
+ * Creates a new projection with the given properties specified by a builder.
- * The {@link #apply(Feature)} method will copy the properties of the given
++ * The {@link #apply(AbstractFeature)} method will copy the properties of the given
+ * features into new instances of {@link #typeWithDependencies}.
+ *
+ * @param typeRequested the type of projected features.
+ * @param projection descriptions of the properties to keep in the projected features.
+ */
- FeatureProjection(final FeatureType typeRequested, final FeatureType typeWithDependencies,
++ FeatureProjection(final DefaultFeatureType typeRequested, final DefaultFeatureType typeWithDependencies,
+ final List<FeatureProjectionBuilder.Item> projection)
+ {
+ this.createInstance = true;
+ this.typeRequested = typeRequested;
+ this.typeWithDependencies = typeWithDependencies;
+ int storedCount = 0;
+
+ // Expressions to apply on the source feature for fetching the property values of the projected feature.
+ @SuppressWarnings({"LocalVariableHidesMemberVariable", "unchecked", "rawtypes"})
- final Expression<? super Feature,?>[] expressions = new Expression[projection.size()];
++ final Expression<? super AbstractFeature,?>[] expressions = new Expression[projection.size()];
+
+ // Names of the properties to be stored in the attributes of the target features.
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
+ final String[] propertiesToCopy = new String[expressions.length];
+
+ for (final FeatureProjectionBuilder.Item item : projection) {
+ final var expression = item.attributeValueGetter();
+ if (expression != null) {
+ expressions[storedCount] = expression;
+ propertiesToCopy[storedCount++] = item.getName();
+ }
+ }
+ this.propertiesToCopy = ArraysExt.resize(propertiesToCopy, storedCount);
+ this.expressions = ArraysExt.resize(expressions, storedCount);
+ }
+
+ /**
+ * Creates a new projection with a subset of the properties of another projection.
+ * This constructor is invoked when the caller handles itself some of the properties.
+ *
+ * <h4>behavioral change</h4>
+ * Projections created by this constructor assumes that the feature instances given to the
- * {@link #apply(Feature)} method are already instances of {@link #typeWithDependencies}
++ * {@link #apply(AbstractFeature)} method are already instances of {@link #typeWithDependencies}
+ * and can be modified (if needed) in place. This constructor is designed for cases where
+ * the caller does itself a part of the {@code FeatureProjection} work.
+ *
+ * @param parent the projection from which to inherit the types and expressions.
+ * @param remaining index of the properties that still need to be copied after the caller did its processing.
+ *
+ * @see #afterPreprocessing(int[])
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ private FeatureProjection(final FeatureProjection parent, final int[] remaining) {
+ createInstance = false;
+ typeRequested = parent.typeRequested;
+ typeWithDependencies = parent.typeWithDependencies;
+ expressions = new Expression[remaining.length];
+ propertiesToCopy = new String[remaining.length];
+ for (int i=0; i<remaining.length; i++) {
+ final int index = remaining[i];
+ propertiesToCopy[i] = parent.propertiesToCopy[index];
+ expressions[i] = parent.expressions[index];
+ }
+ }
+
+ /**
+ * Returns a variant of this projection where the caller has created the target feature instance itself.
+ * The callers is may have set some property values itself, and the {@code remaining} argument gives the
+ * indexes of the properties that still need to be copied after caller's processing.
+ *
+ * @param remaining index of the properties that still need to be copied after the caller did its processing.
+ * @return a variant of this projection which only completes the projection done by the caller,
+ * or {@code null} if there is nothing left to complete.
+ */
+ public FeatureProjection afterPreprocessing(final int[] remaining) {
+ if (remaining.length == 0 && typeRequested == typeWithDependencies) {
+ return null;
+ }
+ return new FeatureProjection(this, remaining);
+ }
+
+ /**
+ * Returns the names of all stored properties. This list may be shorter than the list of properties of the
+ * {@linkplain #typeRequested requested feature type} if some feature properties are computed on-the-fly,
+ * or if the target feature contains some new properties that are not in the source.
+ *
+ * @return the name of all stored properties.
+ */
+ public final List<String> propertiesToCopy() {
+ return UnmodifiableArrayList.wrap(propertiesToCopy);
+ }
+
+ /**
+ * Returns the path to the value (in source features) of the property at the given index.
+ * The argument corresponds to an index in the list returned by {@link #propertiesToCopy()}.
+ * The return value is often the same name as {@code propertiesToCopy().get(index)} but may
+ * differ if the user has specified aliases or if two properties have the same default name.
+ *
+ * @param index index of the stored property for which to get the name in the source feature.
+ * @return path in the source features, or empty if the property is not a {@link ValueReference}.
+ */
+ public Optional<String> xpath(final int index) {
- final Expression<? super Feature, ?> expression = expressions[index];
++ final Expression<? super AbstractFeature, ?> expression = expressions[index];
+ if (expression instanceof ValueReference<?,?>) {
+ return Optional.of(((ValueReference<?,?>) expression).getXPath());
+ }
+ return Optional.empty();
+ }
+
+ /**
+ * Returns all dependencies used, directly or indirectly, by all expressions used in this projection.
+ * The set includes transitive dependencies (expressions with operands that are other expressions).
+ * The elements are XPaths.
+ *
+ * @return all dependencies (including transitive dependencies) as XPaths.
+ */
+ public Set<String> dependencies() {
+ Set<String> references = null;
+ for (var expression : expressions) {
+ references = ListingPropertyVisitor.xpaths(expression, references);
+ }
+ return (references != null) ? references : Set.of();
+ }
+
+ /**
+ * Derives a new projected feature instance from the given source.
+ * The feature type of the returned feature instance will be be {@link #typeRequested}.
+ * This method performs the following steps:
+ *
+ * <ol class="verbose">
+ * <li>If this projection was created by {@link #afterPreprocessing(int[])}, then the given feature
+ * shall be an instances of {@link #typeWithDependencies} and may be modified in-place. Otherwise,
+ * this method creates a new instance of {@link #typeWithDependencies}.</li>
+ * <li>This method executes all expressions for fetching values from {@code source}
+ * and stores the results in the feature instance of above step.</li>
+ * <li>If {@link #typeWithDependencies} is different than {@link #typeRequested}, then the feature
+ * of above step is wrapped in a view which hides the undesired properties.</li>
+ * </ol>
+ *
+ * @param source the source feature instance.
+ * @return the "projected" (<abbr>SQL</abbr> database sense) feature instance.
+ */
+ @Override
- public Feature apply(final Feature source) {
++ public AbstractFeature apply(final AbstractFeature source) {
+ var feature = createInstance ? typeWithDependencies.newInstance() : source;
+ for (int i=0; i < expressions.length; i++) {
+ feature.setPropertyValue(propertiesToCopy[i], expressions[i].apply(source));
+ }
+ if (typeRequested != typeWithDependencies) {
+ feature = new FeatureView(typeRequested, feature);
+ }
+ return feature;
+ }
+
+ /**
+ * Returns a string representation of this projection for debugging purposes.
+ * The current implementation formats a table with all properties, including
+ * dependencies, and a column saying whether the property is an operation,
+ * is stored or whether there is an error.
+ *
+ * @return a string representation.
+ */
+ @Override
+ public String toString() {
+ return Row.toString(this, propertiesToCopy);
+ }
+
+ /**
+ * Helper class for the implementation of {@link FeatureProjection#toString()}.
+ * Each instance represents a row in the table to be formatted..
+ * This is used for debugging purposes only.
+ */
+ @Debug
+ private static final class Row {
+ /**
+ * Returns a string representation of the {@link FeatureProjection} having the given values.
+ * Having this method in a separated class reduces the amount of classes loading, since this
+ * {@code Row} class is rarely needed in production environment.
+ *
+ * @param projection the projection for which to format a string representation.
+ * @param propertiesToCopy value of {@link FeatureProjection#propertiesToCopy}.
+ * @return the string representation of the given projection.
+ */
+ static String toString(final FeatureProjection projection, final String[] propertiesToCopy) {
+ final var rowByName = new LinkedHashMap<String,Row>();
+ if (projection.typeWithDependencies != projection.typeRequested) {
+ addAll(rowByName, projection.typeWithDependencies, "dependency");
+ }
+ addAll(rowByName, projection.typeRequested, "operation"); // Overwrite above dependencies.
+ for (int i=0; i < propertiesToCopy.length; i++) {
+ String name = propertiesToCopy[i];
+ String value;
+ try {
+ name = projection.typeWithDependencies.getProperty(name).getName().toString();
+ value = "stored";
+ } catch (RuntimeException e) {
+ value = e.toString();
+ }
+ Row row = rowByName.computeIfAbsent(name, Row::new);
+ row.type = value;
+ row.xpath = projection.xpath(i).orElse("");
+ }
+ final var words = Vocabulary.forLocale(null);
+ final var table = new TableAppender(" │ ");
+ table.setMultiLinesCells(true);
+ table.appendHorizontalSeparator();
+ table.append(words.getString(Vocabulary.Keys.Property)).nextColumn();
+ table.append(words.getString(Vocabulary.Keys.Type)).nextColumn();
+ table.append("XPath").nextLine();
+ table.appendHorizontalSeparator();
+ for (final Row row : rowByName.values()) {
+ table.append(row.property).nextColumn();
+ table.append(row.type).nextColumn();
+ table.append(row.xpath).nextLine();
+ }
+ table.appendHorizontalSeparator();
+ return table.toString();
+ }
+
+ /**
+ * Adds all properties of the given feature type into the specified map.
+ * For each property, {@link #type} is overwritten with the {@code type} argument value.
+ *
+ * @param rowByName where to add the properties.
+ * @param featureType the type from which to get the list of properties.
+ * @param type the "stored", "operation" or "dependency" value to assign to {@link #type}.
+ */
- private static void addAll(final Map<String,Row> rowByName, final FeatureType featureType, final String type) {
++ private static void addAll(final Map<String,Row> rowByName, final DefaultFeatureType featureType, final String type) {
+ for (final var property : featureType.getProperties(true)) {
+ Row row = rowByName.computeIfAbsent(property.getName().toString(), Row::new);
+ row.type = type;
+ }
+ }
+
+ /**
+ * Creates a row with no value.
+ *
+ * @param name number under which the property is stored.
+ */
+ private Row(final String name) {
+ property = name;
+ xpath = "";
+ }
+
+ /**
+ * Name of the property in the projected feature type.
+ */
+ private final String property;
+
+ /**
+ * Path to the property value in the source feature.
+ */
+ private String xpath;
+
+ /**
+ * Type: stored, operation or dependency.
+ */
+ private String type;
+ }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
index 0000000000,2f9c7bd6df..d3d6af82db
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureProjectionBuilder.java
@@@ -1,0 -1,741 +1,739 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ package org.apache.sis.feature.privy;
+
+ import java.util.Map;
+ import java.util.HashMap;
+ import java.util.List;
+ import java.util.ArrayList;
+ import java.util.Iterator;
+ import java.util.Locale;
+ import java.util.Objects;
+ import java.util.Optional;
+ import java.util.function.UnaryOperator;
+ import org.opengis.util.GenericName;
+ import org.opengis.referencing.crs.CoordinateReferenceSystem;
+ import org.apache.sis.feature.Features;
+ import org.apache.sis.feature.AbstractOperation;
+ import org.apache.sis.feature.FeatureOperations;
+ import org.apache.sis.feature.builder.AssociationRoleBuilder;
+ import org.apache.sis.feature.builder.AttributeTypeBuilder;
+ import org.apache.sis.feature.builder.FeatureTypeBuilder;
+ import org.apache.sis.feature.builder.PropertyTypeBuilder;
+ import org.apache.sis.util.ArgumentCheckByAssertion;
+ import org.apache.sis.util.UnconvertibleObjectException;
+ import org.apache.sis.util.privy.Strings;
+ import org.apache.sis.util.resources.Errors;
+ import org.apache.sis.util.resources.Vocabulary;
+
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.PropertyType;
-import org.opengis.feature.IdentifiedType;
-import org.opengis.feature.Operation;
-import org.opengis.filter.Expression;
-import org.opengis.filter.ValueReference;
++// Specific to the main branch:
++import org.apache.sis.filter.Expression;
++import org.apache.sis.feature.AbstractFeature;
++import org.apache.sis.feature.DefaultFeatureType;
++import org.apache.sis.feature.DefaultAttributeType;
++import org.apache.sis.feature.AbstractIdentifiedType;
++import org.apache.sis.pending.geoapi.filter.ValueReference;
+
+
+ /**
+ * A builder for deriving a feature type containing a subset of the properties of another type.
+ * The other type is called the {@linkplain #source() source} feature type. The properties that
+ * are retained may have different names than the property names of the source feature type.
+ * If a property is a link such as {@code sis:identifier} or {@code sis:geometry},
+ * this class keeps trace of the dependencies required for recreating the link.
+ *
+ * <p>Properties that are copied from the source feature type are declared by calls to
- * {@link #addSourceProperty(PropertyType, boolean)} and related methods defined in this class.
++ * {@link #addSourceProperty(AbstractIdentifiedType, boolean)} and related methods defined in this class.
+ * The methods inherited from the parent class can also be invoked,
+ * but they will receive no special treatment.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+ public final class FeatureProjectionBuilder extends FeatureTypeBuilder {
+ /**
+ * The type of features that provide the values to store in the projected features.
+ * The value of this field does not change, except when following a XPath such as {@code "a/b/c"}.
+ *
+ * @see #source()
+ */
- private FeatureType source;
++ private DefaultFeatureType source;
+
+ /**
+ * Whether the source is a dependency of the feature type given to the constructor.
+ * This flag become {@code true} when following a XPath of the form {@code "a/b/c"}.
+ * In such case, {@link #source} may be temporarily set to the tip {@code "c"} type.
+ *
- * @see #using(FeatureType, FeatureExpression)
++ * @see #using(DefaultFeatureType, FeatureExpression)
+ */
+ private boolean sourceIsDependency;
+
+ /**
+ * The properties to inherit from the {@linkplain #source} feature type by explicit user's request.
+ * The property types and the property names are not necessarily the same as in the source feature.
+ * For example, an operation may be replaced by an attribute which will store the operation result.
+ *
+ * <h4>Implementation note</h4>
+ * This collection cannot be a {@link java.util.Map} with names of the source properties as keys,
+ * because the list may contain more than one item with the same {@link Item#sourceName} value.
+ * This collision happens if some items are the results of XPath evaluations such as {@code "a/b/c"}.
+ * This is the reason why properties can be renamed before to be stored in the projected features.
+ */
+ private final List<Item> requested;
+
+ /**
+ * Names that are actually used, or may be used, in the projected feature type.
+ * For each name, the associated value is the item that is explicitly using that name.
+ * A {@code null} value means that the name is not used, but is nevertheless reserved
+ * because potentially ambiguous. This information is used for avoiding name collisions
+ * in automatically generated names.
+ *
+ * <p>Note that the keys are not necessarily the values of {@link Item#sourceName}.
+ * Keys are rather the values of {@code Item.builder.getName()}, except that the
+ * latter may not be valid before {@link Item#validateName()} is invoked.</p>
+ *
+ * @see #reserve(GenericName, Item)
+ */
+ private final Map<GenericName, Item> reservedNames;
+
+ /**
+ * Whether at least one item 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()
+ */
+ private boolean hasModifiedProperties;
+
+ /**
+ * Sequential number for generating default names of unnamed properties. This is used when the
+ * name inherited from the {@linkplain #source} feature is unknown or collides with another name.
+ */
+ private int unnamedNumber;
+
+ /**
+ * Names of {@linkplain #source} properties that are dependencies found in operations.
+ * The most common cases are the targets of {@code "sis:identifier"} and {@code "sis:geometry"} links.
+ * Values are the items having this dependency. Many items may share the same dependency.
+ *
+ * <p>At first, this map is populated without checking if the properties requested by the user contains
+ * those dependencies. After all user's requested properties have been declared to this builder, values
+ * are filtered for identifying which dependencies need to be added as implicit properties.</p>
+ */
+ private final Map<String, List<Item>> dependencies;
+
+ /**
+ * Creates a new builder instance using the default factories.
+ *
+ * @todo provides a way to specify the factories used by the data store.
+ *
+ * @param source the type from which to take the properties to keep in the projected feature.
+ * @param locale the locale to use for formatting error messages, or {@code null} for the default locale.
+ */
- public FeatureProjectionBuilder(final FeatureType source, final Locale locale) {
++ public FeatureProjectionBuilder(final DefaultFeatureType source, final Locale locale) {
+ super(null, null, locale);
+ this.source = Objects.requireNonNull(source);
+ requested = new ArrayList<>();
+ dependencies = new HashMap<>();
+ reservedNames = new HashMap<>();
+ }
+
+ /**
+ * Returns the type of features that provide the values to store in the projected features.
+ * This is the type given at construction time, except when following a XPath such as
+ * {@code "a/b/c"} in which case it may temporarily be the leaf {@code "c"} type.
+ *
+ * @return the current source of properties (never {@code null}).
+ */
- public FeatureType source() {
++ public DefaultFeatureType source() {
+ return source;
+ }
+
+ /**
+ * Returns the expected type of the given expression using the given feature type as the source.
+ * This method temporarily sets the {@linkplain #source() source} to the given {@code childType},
+ * then returns the value of {@code expression.expectedType(this)}.
+ * This is used for the last expression in a XPath such as {@code "a/b/c"}.
+ *
+ * @param childType the feature type to use.
+ * @param expression the expression from which to get the expected type.
+ * @return the expected type, or {@code null}.
+ *
+ * @see FeatureExpression#expectedType(FeatureProjectionBuilder)
+ */
- public Item using(final FeatureType childType, final FeatureExpression<?,?> expression) {
- final FeatureType previous = source;
++ public Item using(final DefaultFeatureType childType, final FeatureExpression<?,?> expression) {
++ final DefaultFeatureType previous = source;
+ final boolean status = sourceIsDependency;
+ try {
+ sourceIsDependency = true;
+ source = Objects.requireNonNull(childType);
+ return expression.expectedType(this);
+ } finally {
+ source = previous;
+ sourceIsDependency = status;
+ }
+ }
+
+ /**
+ * Adds the given property, replacing operation by an attribute storing the operation result.
+ * This method may return {@code null} if it cannot resolve the property type, in which case
+ * the caller should throw an exception (throwing an exception is left to the caller because
+ * it can produces a better error message). Operation's dependencies, if any, are added into
+ * the given {@code deferred} list.
+ *
+ * @param property the {@linkplain #source} property to add.
+ * @param deferred where to add operation's dependencies, or {@code null} for not collecting dependencies.
+ * @return builder for the projected property, or {@code null} if it cannot be resolved.
+ */
- private PropertyTypeBuilder addPropertyResult(PropertyType property, final List<String> deferred) {
- if (property instanceof Operation) {
++ private PropertyTypeBuilder addPropertyResult(AbstractIdentifiedType property, final List<String> deferred) {
++ if (property instanceof AbstractOperation) {
+ final GenericName name = property.getName();
+ do {
+ if (deferred != null) {
+ if (property instanceof AbstractOperation) {
+ deferred.addAll(((AbstractOperation) property).getDependencies());
+ } else {
+ /*
+ * Cannot resolve dependencies. Current implementation assumes that there is no dependency.
+ * Note: we could conservatively add all properties as dependencies, but this is difficult
+ * to implement efficiently.
+ */
+ }
+ }
- final IdentifiedType result = ((Operation) property).getResult();
- if (result != property && result instanceof PropertyType) {
- property = (PropertyType) result;
- } else if (result instanceof FeatureType) {
- return addAssociation((FeatureType) result).setName(name);
++ final AbstractIdentifiedType result = ((AbstractOperation) property).getResult();
++ if (result != property && result instanceof AbstractIdentifiedType) {
++ property = result;
++ } else if (result instanceof DefaultFeatureType) {
++ return addAssociation((DefaultFeatureType) result).setName(name);
+ } else {
+ return null;
+ }
- } while (property instanceof Operation);
++ } while (property instanceof AbstractOperation);
+ return addProperty(property).setName(name);
+ }
+ return addProperty(property);
+ }
+
+ /**
+ * Adds a property from the source feature type. The given property should be the result of a call to
+ * {@code source().getProperty(sourceName)}. The call to {@code getProperty(…)} is left to the caller
+ * because some callers need to wrap that call in a {@code try} block.
+ *
+ * @param property the property type, usually as one of the properties of {@link #source()}.
+ * @param named whether the {@code property} name can be used as a default name.
+ * @return handler for the given item, or {@code null} if the given property cannot be resolved.
+ */
- public Item addSourceProperty(final PropertyType property, final boolean named) {
++ public Item addSourceProperty(final AbstractIdentifiedType property, final boolean named) {
+ if (property == null) {
+ return null;
+ }
+ final PropertyTypeBuilder builder;
+ List<String> deferred;
+ if (sourceIsDependency) {
+ /*
+ * Adding a property which is not defined in the feature type specified at construction time,
+ * but which is defined at the tip of some XPath such as "a/b/c". This is not the same thing
+ * as adding an association. This is rather adding a subset of an association. We do not add
+ * dependency information because the dependencies are not directly in the source feature.
+ */
+ reserve(property.getName(), null);
+ deferred = new ArrayList<>();
+ builder = addPropertyResult(property, deferred);
+ } else {
+ /*
+ * For link operations, remember the dependencies in order to determine (after we added all properties)
+ * if we can keep the property as an operation or if we will need to copy the value in an attribute.
+ * For other kind of operations, unconditionally replace the operation by its result.
+ */
+ deferred = Features.getLinkTargets(property);
+ if (deferred.isEmpty()) {
+ deferred = new ArrayList<>();
+ builder = addPropertyResult(property, deferred);
+ } else {
+ builder = addProperty(property);
+ }
+ }
+ final var item = new Item(named ? property.getName() : null, builder);
+ requested.add(item);
+ for (String dependency : deferred) {
+ dependencies.computeIfAbsent(dependency, (key) -> new ArrayList<>(2)).add(item);
+ }
+ return item;
+ }
+
+ /**
+ * Adds a property created by the caller rather than extracted from the source feature.
+ * The given builder should have been created by a method of the {@link FeatureTypeBuilder} parent class.
+ * The name of the builder is usually not the name of a property in the {@linkplain #source() source} feature.
+ *
+ * <h4>Assertions</h4>
+ * This method verifies that the given builder is a member of the {@linkplain #properties() properties} collection.
+ * 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 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.
+ */
+ @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);
+ requested.add(item);
+ return item;
+ }
+
+ /**
+ * Handler for a property inherited from the source feature type. The property is initially unnamed.
+ * A name can be specified explicitly after construction by a call to {@link #setName(GenericName)}.
+ * If no name is specified, the default name will be the same as in the source feature type if that
+ * name is available, or a default name otherwise.
+ */
+ public final class Item {
+ /**
+ * The name that the property had in the {@linkplain #source() source} feature, or {@code null}.
+ * The property built by the {@linkplain #builder} will often have the same name, but not always.
+ */
+ final GenericName sourceName;
+
+ /**
+ * The builder for configuring the property.
+ */
+ private PropertyTypeBuilder builder;
+
+ /**
+ * Whether this item got an explicit name. The specified name may be
+ * identical to the name in the {@linkplain #source() source} feature.
+ */
+ private boolean isNamed;
+
+ /**
+ * Whether to keep the current name if it is available. This is set to {@code true} when user did not
+ * specified explicitly a name, but keeping the name of the source property would be a natural choice.
+ * However, before to use that name, we need to wait and see if that name will be explicitly used for
+ * another property.
+ */
+ private boolean preferCurrentName;
+
+ /**
+ * Whether this property needs at least one dependency which is not included in the list of properties
+ * requested by the user. In such case, we cannot keep the link operation and need to replace the link
+ * by a stored attribute.
+ *
+ * @see #replaceIfMissingDependency()
+ */
+ private boolean hasMissingDependency;
+
+ /**
+ * Expression for evaluating the attribute value from a source feature instance, or {@code null} if none.
+ * This field should be non-null only if the value will be stored in an attribute. If the property is an
+ * operation, then this field should be null (this is not the expression of the operation).
+ *
+ * @see #attributeValueGetter()
+ */
- private Expression<? super Feature, ?> attributeValueGetter;
++ private Expression<? super AbstractFeature, ?> attributeValueGetter;
+
+ /**
+ * Creates a new handle for the property created by the given builder.
+ *
+ * @param sourceName the property name in the {@linkplain #source() source} feature, or {@code null}.
+ * @param builder the builder for configuring the property.
+ */
+ private Item(final GenericName sourceName, final PropertyTypeBuilder builder) {
+ this.sourceName = sourceName;
+ this.builder = builder;
+ }
+
+ /**
+ * Returns a string representation for debugging purposes.
+ */
+ @Override
+ public String toString() {
+ return Strings.toString(getClass(),
+ "sourceName", (sourceName != null) ? sourceName.toString() : null,
+ "targetName", isNamed ? getName() : null,
+ "valueClass", (builder instanceof AttributeTypeBuilder<?>) ? ((AttributeTypeBuilder<?>) builder).getValueClass() : null,
+ null, hasMissingDependency ? "hasMissingDependency" : null);
+ }
+
+ /**
+ * Returns the property type builder wrapped by this item.
+ * The following operations are allowed on the returned builder:
+ *
+ * <ul>
+ * <li>Set the cardinality (minimum and maximum occurrences).</li>
+ * <li>Build the {@code PropertyType}.</li>
+ * </ul>
+ *
+ * The following operations should <em>not</em> be executed on the returned builder.
+ * Use the dedicated methods in this class instead:
+ *
+ * <ul>
+ * <li>Set the name: use {@link #setName(GenericName)}.</li>
+ * <li>Set the value class: use {@link #replaceValueClass(UnaryOperator)}.</li>
+ * </ul>
+ *
+ * @return the property type builder wrapped by this item.
+ */
+ public PropertyTypeBuilder builder() {
+ hasModifiedProperties = true; // Conservative because the caller may do anything on the builder.
+ return builder;
+ }
+
+ /**
+ * Replaces this property by a stored attribute if at least one dependency is not in the list of properties
+ * requested by the user. This method should be invoked only for preparing the user requested feature type.
+ * This method should not be invoked for preparing the feature type with dependencies, because the latter
+ * should contain the missing dependencies.
+ */
+ private void replaceIfMissingDependency() {
+ if (hasMissingDependency) {
+ hasMissingDependency = false;
+ hasModifiedProperties = true;
+ final var old = builder;
+ builder = addPropertyResult(old.build(), null); // `old.build()` returns the existing operation.
+ old.replaceBy(builder);
+ }
+ }
+
+ /**
+ * Sets the class of attribute values. If the builder is an instance of {@link AttributeTypeBuilder}
+ * and if {@code type.apply(valueClass)} returns a non-null value ({@code valueClass} is the current
+ * class of attribute values), then this method sets the new attribute value class to the specified
+ * type and returns {@code true}. Otherwise, this method returns {@code false}.
+ *
+ * @param type a converter from current class to the new class of attribute values.
+ * @return whether the value class has been set to the value returned by {@code type}.
+ * @throws UnconvertibleObjectException if the default value cannot be converted to the given type.
+ */
+ public boolean replaceValueClass(final UnaryOperator<Class<?>> type) {
+ if (builder instanceof AttributeTypeBuilder<?>) {
+ final var ab = (AttributeTypeBuilder<?>) builder;
+ final Class<?> r = type.apply(ab.getValueClass());
+ if (r != null) {
+ if (builder != (builder = ab.setValueClass(r))) {
+ hasModifiedProperties = true;
+ }
+ return true;
+ }
+ } else if (builder instanceof AssociationRoleBuilder) {
+ // We do not yet have a special case for this one.
+ } else {
+ final var property = builder.build();
- if (property instanceof Operation) {
++ if (property instanceof AbstractOperation) {
+ /*
+ * Less common case where the caller wants to change the type of an operation.
+ * We cannot change the type of an operation (unless we replace the operation
+ * by a stored attribute). Therefore, we only check type compatibility.
+ */
- final var result = ((Operation) property).getResult();
- if (result instanceof AttributeType<?>) {
- final Class<?> c = ((AttributeType<?>) result).getValueClass();
++ final var result = ((AbstractOperation) property).getResult();
++ if (result instanceof DefaultAttributeType<?>) {
++ final Class<?> c = ((DefaultAttributeType<?>) result).getValueClass();
+ final Class<?> r = type.apply(c);
+ if (r != null) {
+ // We can be lenient for link operation, but must be strict for other operations.
+ if (Features.getLinkTarget(property).isPresent() ? r.isAssignableFrom(c) : r.equals(c)) {
+ return true;
+ }
+ throw new UnconvertibleObjectException(Errors.forLocale(getLocale())
+ .getString(Errors.Keys.CanNotConvertFromType_2, c, r));
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sets the expression to use for evaluating the property value.
+ * If {@code stored} is {@code true} (the usual case), then the expression will be evaluated early
+ * and its result will be stored as an attribute value, unless this property is not an attribute.
+ * If {@code stored} is {@code false}, this method replaces the attribute by an operation wrapping
+ * the given expression. In other words, the evaluation of the expression will be deferred.
+ * The latter case is possible only if the {@code FeatureType} contains all dependencies
+ * that the operation needs.
+ *
+ * @param expression the expression to be evaluated by the operation.
+ */
- public void setValueGetter(final Expression<? super Feature, ?> expression, final boolean stored) {
++ public void setValueGetter(final Expression<? super AbstractFeature, ?> expression, final boolean stored) {
+ if (builder instanceof AttributeTypeBuilder<?>) {
+ if (stored) {
+ attributeValueGetter = expression;
+ } else {
+ final var atb = (AttributeTypeBuilder<?>) builder;
+ /*
+ * Optimization: we could compute `storedType = atb.build()` unconditionally,
+ * which creates an attribute with the final name in the target feature type.
+ * However, in the particular case of links, we are better to use the name of
+ * the property in the source feature type, because it allows an optimization
+ * in `ExpressionOperation.create(…)` (a replacement by a `LinkOperation`).
+ */
- AttributeType<?> storedType = null;
++ DefaultAttributeType<?> storedType = null;
+ if (expression instanceof ValueReference<?,?>) {
+ var candidate = source.getProperty(((ValueReference<?,?>) expression).getXPath());
- if (candidate instanceof AttributeType<?>) {
- storedType = (AttributeType<?>) candidate;
++ if (candidate instanceof DefaultAttributeType<?>) {
++ storedType = (DefaultAttributeType<?>) 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.
+ }
+ }
+
+ /**
+ * Returns the expression for evaluating the value to store in the attribute built by this item.
+ * The expression may be {@code null} if the value is computed on-the-fly (i.e. the property is
+ * an operation), or if the expression has not been specified.
+ */
- final Expression<? super Feature, ?> attributeValueGetter() {
++ final Expression<? super AbstractFeature, ?> attributeValueGetter() {
+ return attributeValueGetter;
+ }
+
+ /**
+ * Sets the coordinate reference system that characterizes the values of this attribute.
+ *
+ * @param crs coordinate reference system associated to attribute values, or {@code null}.
+ * @return {@code this} for method calls chaining.
+ */
+ public Item setCRS(final CoordinateReferenceSystem crs) {
+ if (builder instanceof AttributeTypeBuilder<?>) {
+ builder = ((AttributeTypeBuilder<?>) builder).setCRS(crs);
+ hasModifiedProperties = true;
+ }
+ return this;
+ }
+
+ /**
+ * Returns whether the property built by this item is equivalent to the given property.
+ * The caller should have verified that {@link #hasModifiedProperties} is {@code false}
+ * before to invoke this method, because the implementation performs a filtering based
+ * on the property name only. This is that way for accepting differences in metadata.
+ *
+ * @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) {
++ private boolean equivalent(final AbstractIdentifiedType property) {
+ return builder.getName().equals(property.getName());
+ }
+
+ /**
+ * Returns the name of the projected property.
+ * This is initially the name of the property given at construction time,
+ * but can be changed later by a call to {@link #setName(GenericName)}.
+ *
+ * @return the name of the projected property.
+ */
+ public String getName() {
+ return builder.getName().toString();
+ }
+
+ /**
+ * Sets the name of the projected property. A {@code null} argument means that the name is unspecified,
+ * in which case a different name may be generated later if the current name collides with other names.
+ *
+ * <p>This method should be invoked exactly once for each item, even if the argument is {@code null}.
+ * The reason is because this method uses this information for recording which names to reserve.</p>
+ *
+ * @param targetName the desired name in the projected feature, or {@code null} if unspecified.
+ */
+ public void setName(final GenericName targetName) {
+ if (targetName == null) {
+ reserve(sourceName, null); // Will use that name only if not owned by another item.
+ preferCurrentName = true;
+ } else if (targetName.equals(sourceName)) {
+ reserve(sourceName, this); // Take possession of that name.
+ isNamed = true;
+ } else {
+ builder.setName(targetName);
+ reserve(targetName, this);
+ hasModifiedProperties = true; // Because the name is different.
+ isNamed = true;
+ }
+ }
+
+ /**
+ * If this item has not received an explicit name, infers a default name.
+ * This method should be invoked only after {@link #setName(GenericName)}
+ * has been invoked for all items, for allowing this class to know which
+ * names are reserved.
+ */
+ private void validateName() {
+ if (!isNamed) {
+ final Item owner = reservedNames.get(sourceName);
+ if (owner != this) {
+ GenericName name = sourceName;
+ if (owner != null || name == null || (!preferCurrentName && reservedNames.containsKey(name))) {
+ do {
+ var text = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed_1, ++unnamedNumber);
+ name = builder.setName(text).getName(); // Local name with the appropriate name space.
+ } while (reservedNames.containsKey(name)); // Reminder: the associated value may be null.
+ }
+ reserve(name, this);
+ }
+ isNamed = true;
+ }
+ }
+ }
+
+ /**
+ * Declares the given name as reserved. If this class needs to generate a default name,
+ * it will ensure that automatically generated names do not conflict with reserved names.
+ *
+ * @param name name to reserve for a projected property type, or {@code null} if none.
+ * @param owner the builder using that name, or {@code null} if none.
+ */
+ private void reserve(GenericName name, final Item owner) {
+ if (name != null) {
+ // By `putIfAbsent` method contract, non-null values have precedence over null values.
+ reservedNames.putIfAbsent(name, owner);
+ if (name != (name = name.tip())) { // Shortcut for a majority of cases.
+ reservedNames.putIfAbsent(name, owner);
+ }
+ }
+ }
+
+ /**
+ * Adds dependencies. This method adds in the {@code deferred} list any transitive
+ * dependencies which may need to be added in a second pass after this method call.
+ * The elements added into {@code deferred} are {@linkplain #source} properties.
+ *
+ * @param deferred where to add missing transitive dependencies (source properties).
+ */
- private void resolveDependencies(final List<PropertyType> deferred) {
++ private void resolveDependencies(final List<AbstractIdentifiedType> 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 AbstractIdentifiedType property = source.getProperty(entry.getKey());
+ final GenericName sourceName = property.getName();
+ Item item = reservedNames.get(sourceName);
+ if (item != null) {
+ if (!sourceName.equals(item.sourceName)) {
+ /*
+ * If we want to support that feature in a future version, we would need a `replace` method
+ * for replacing a builder at a specific index or for a specific property name. A difficulty
+ * is that for compound identifiers, we have no API for reusing the same prefix and suffix.
+ */
+ throw new UnsupportedOperationException("Renaming of properties used in links is not yet supported.");
+ }
+ } else {
+ for (Item dependent : entry.getValue()) {
+ dependent.hasMissingDependency = true;
+ }
+ deferred.add(property);
+ }
+ it.remove();
+ }
+ }
+
+ /**
+ * Returns {@code true} if the feature to be built should be equivalent to the source feature.
+ *
+ * @return whether the {@linkplain #source} feature type can be used directly.
+ */
+ private boolean isIdentity() {
+ if (hasModifiedProperties) {
+ return false;
+ }
+ final Iterator<Item> it = requested.iterator();
- for (PropertyType property : source.getProperties(true)) {
++ for (AbstractIdentifiedType 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() {
++ public DefaultFeatureType build() {
+ return isIdentity() ? source : super.build();
+ }
+
+ /**
+ * Sets the default name of all anonymous properties, then builds the feature types.
+ * Two feature types are built: one with only the requested properties, and another
+ * type augmented with dependencies of operations such as links.
+ *
+ * <p>This method should be invoked exactly once.</p>
+ *
+ * <h4>Identity operation</h4>
+ * If the result is a feature type with all the properties of the source feature,
+ * with the same property names in the same order, and if the expressions are only
+ * fetching the values (no computation), then this method returns an empty value
+ * for meaning that this projection does nothing.
+ *
+ * @return the feature types with and without dependencies, or empty if there is no projection.
+ */
+ public Optional<FeatureProjection> project() {
+ requested.forEach(Item::validateName);
+ /*
+ * Add properties for all dependencies that are required by link operations but are not already present.
+ * If there is no need to add anything, `typeWithDependencies` will be directly the feature type to return.
+ */
+ final List<PropertyTypeBuilder> properties = properties();
+ final int count = properties.size();
- final var deferred = new ArrayList<PropertyType>();
++ final var deferred = new ArrayList<AbstractIdentifiedType>();
+ 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;
++ final DefaultFeatureType typeRequested, typeWithDependencies;
+ if (deferred.isEmpty()) {
+ typeRequested = typeWithDependencies = build();
+ } else {
+ do {
- for (PropertyType property : deferred) {
++ for (AbstractIdentifiedType 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.
+ requested.forEach(Item::replaceIfMissingDependency);
+ typeRequested = build();
+ }
+ if (source.equals(typeRequested) && source.equals(typeWithDependencies)) {
+ return Optional.empty();
+ }
+ return Optional.of(new FeatureProjection(typeRequested, typeWithDependencies, requested));
+ }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureView.java
index 0000000000,a0dac0a57f..f8c5a97a49
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureView.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/privy/FeatureView.java
@@@ -1,0 -1,131 +1,129 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ package org.apache.sis.feature.privy;
+
+ import org.apache.sis.feature.AbstractFeature;
+
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.Property;
++// Specific to the main branch:
++import org.apache.sis.feature.DefaultFeatureType;
+
+
+ /**
+ * A feature containing a subset of the properties of another feature.
+ * The feature type of the view must be specified in argument together with the source feature instance.
+ * All properties that are present in this feature view shall have the same name as in the source feature.
+ * This class does not verify that requirement.
+ *
+ * <h2>Limitations</h2>
+ * For performance and simplicity reasons, the current implementation does not prevent users
+ * from requesting a property that exists in the full feature instance but not in this subset.
+ *
+ * <h2>Possible evolution</h2>
+ * It would be possible for the view to contain operations that are not present in the source features.
+ * This extension has not yet been implemented because not yet needed.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+ final class FeatureView extends AbstractFeature {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = -4299168913599597991L;
+
+ /**
+ * The instance with all properties.
+ */
+ @SuppressWarnings("serial") // Apache SIS implementations are serializable.
- private final Feature source;
++ private final AbstractFeature source;
+
+ /**
+ * Creates a new feature instance which is a subset of the given {@code source} feature.
+ * This constructor does not verify the consistency of the two arguments.
+ *
+ * @param subset the feature type that describes the feature subset.
+ * @param source the complete feature instance to view as a subset.
+ */
- FeatureView(final FeatureType subset, final Feature source) {
++ FeatureView(final DefaultFeatureType subset, final AbstractFeature source) {
+ super(subset);
+ this.source = source;
+ }
+
+ /**
+ * Returns the property (attribute, feature association or operation result) of the given name.
+ * This method delegates to the wrapped source without checking whether the given name exists in this subset.
+ *
+ * @param name the property name.
+ * @return the property of the given name (never {@code null}).
+ */
+ @Override
- public Property getProperty(final String name) {
++ public Object getProperty(final String name) {
+ return source.getProperty(name);
+ }
+
+ /**
+ * Sets the property (attribute or feature association).
+ * This method delegates to the wrapped source without checking whether the given name exists in this subset.
+ *
+ * @param property the property to set.
+ */
+ @Override
- public void setProperty(final Property property) {
++ public void setProperty(final Object property) {
+ source.setProperty(property);
+ }
+
+ /**
+ * Returns the value for the property of the given name.
+ * This method delegates to the wrapped source without checking whether the given name exists in this subset.
+ *
+ * @param name the property name.
+ * @return value of the specified property, or the default value (which may be {@code null}} if none.
+ */
+ @Override
+ public Object getPropertyValue(final String name) {
+ return source.getPropertyValue(name);
+ }
+
+ /**
+ * Sets the value for the property of the given name.
+ * This method delegates to the wrapped source without checking whether the given name exists in this subset.
+ *
+ * @param name the property name.
+ * @param value the new value for the specified property (may be {@code null}).
+ */
+ @Override
+ public void setPropertyValue(final String name, final Object value) {
+ source.setPropertyValue(name, value);
+ }
+
+ /**
+ * Returns the value for the property of the given name if that property exists, or a fallback value otherwise.
+ * This method delegates to the wrapped source without checking whether the given name exists in this subset.
+ *
+ * <h4>Design note</h4>
+ * We could add a verification of whether the property exists in the feature type given by {@link #getType()}.
+ * We don't do that for now because the current usages of this method in the Apache SIS code base do not need
+ * this method to be strict, and for consistency with the behavior of other methods in this class.
+ *
+ * @param name the property name.
+ * @param missingPropertyFallback the value to return if no attribute or association of the given name exists.
+ * @return value or default value of the specified property, or {@code missingPropertyFallback}.
+ */
+ @Override
+ public Object getValueOrFallback(final String name, final Object missingPropertyFallback) {
+ return source.getValueOrFallback(name, missingPropertyFallback);
+ }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
index cddc6af90c,eabd8cdf58..1ad4f4f944
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ArithmeticFunction.java
@@@ -27,9 -26,9 +26,8 @@@ import org.apache.sis.util.Unconvertibl
import org.apache.sis.util.resources.Errors;
import org.apache.sis.math.Fraction;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.AttributeType;
-import org.opengis.filter.Expression;
+// Specific to the main branch:
- import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.feature.DefaultAttributeType;
/**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
index 8e44067fff,1ab716a0a3..43b9a49c15
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java
@@@ -22,18 -22,17 +22,17 @@@ import java.util.List
import java.util.Collection;
import java.util.Optional;
import org.apache.sis.feature.Features;
- import org.apache.sis.feature.builder.FeatureTypeBuilder;
- import org.apache.sis.feature.builder.PropertyTypeBuilder;
+ import org.apache.sis.feature.privy.FeatureProjectionBuilder;
import org.apache.sis.math.FunctionProperty;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureAssociationRole;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyType;
-import org.opengis.feature.PropertyNotFoundException;
-import org.opengis.filter.Expression;
-import org.opengis.filter.ValueReference;
+// Specific to the main branch:
+import org.opengis.util.ScopedName;
+import org.apache.sis.feature.AbstractFeature;
+import org.apache.sis.feature.AbstractIdentifiedType;
+import org.apache.sis.feature.DefaultAssociationRole;
+import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.pending.geoapi.filter.Name;
+import org.apache.sis.pending.geoapi.filter.ValueReference;
/**
@@@ -207,32 -201,33 +206,33 @@@ walk: if (specifiedType != null) try
}
/**
- * Provides the expected type of values produced by this expression when a feature of the given type is evaluated.
+ * Provides the expected type of values produced by this expression. This method delegates to
+ * {@link #accessor} after setting the source feature type to the tip of {@code "a/b/c"} path.
*
- * @param valueType the type of features to be evaluated by the given expression.
- * @param addTo where to add the type of properties evaluated by the given expression.
+ * @param addTo where to add the type of properties evaluated by this expression.
* @return builder of the added property, or {@code null} if this method cannot add a property.
- * @throws IllegalArgumentException if this method cannot determine the property type for the given feature type.
- * @throws PropertyNotFoundException if the property was not found in {@code addTo.source()}.
++ * @throws IllegalArgumentException if the property was not found in {@code addTo.source()}.
*/
@Override
- public PropertyTypeBuilder expectedType(DefaultFeatureType valueType, final FeatureTypeBuilder addTo) {
+ public FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
- FeatureType valueType = addTo.source();
++ DefaultFeatureType valueType = addTo.source();
for (final String p : path) {
- final PropertyType type;
+ final AbstractIdentifiedType type;
try {
type = valueType.getProperty(p);
- } catch (PropertyNotFoundException e) {
+ } catch (IllegalArgumentException e) {
if (accessor.isVirtual) {
// The association does not exist but may be defined on a yet unknown child type.
- return accessor.expectedType(addTo);
+ return accessor.defaultType(addTo);
}
throw e;
}
- if (!(type instanceof FeatureAssociationRole)) {
+ if (!(type instanceof DefaultAssociationRole)) {
return null;
}
- valueType = ((FeatureAssociationRole) type).getValueType();
+ valueType = ((DefaultAssociationRole) type).getValueType();
}
- return accessor.expectedType(valueType, addTo);
+ return addTo.using(valueType, accessor);
}
/**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
index 3275c03e5d,c614215ed9..354bac4286
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ConvertFunction.java
@@@ -30,9 -28,9 +28,6 @@@ import org.apache.sis.feature.privy.Fea
import org.apache.sis.math.FunctionProperty;
import org.apache.sis.util.resources.Errors;
- // Specific to the main branch:
- import org.apache.sis.feature.DefaultFeatureType;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.filter.Expression;
--
/**
* Expression whose results are converted to a different type.
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
index 03264883a2,9133bec01e..1c39a4bd3b
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
@@@ -32,13 -33,16 +33,12 @@@ import org.apache.sis.filter.sqlmm.Regi
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.iso.AbstractFactory;
import org.apache.sis.util.resources.Errors;
--import org.apache.sis.util.privy.Strings;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import java.util.Iterator;
-import java.time.Instant;
-import org.opengis.filter.*;
-import org.opengis.feature.Feature;
-import org.opengis.filter.capability.AvailableFunction;
-import org.opengis.filter.capability.FilterCapabilities;
-import org.apache.sis.util.privy.AbstractMap;
+// Specific to the main branch:
+import org.apache.sis.feature.AbstractFeature;
+import org.apache.sis.pending.geoapi.filter.MatchAction;
+import org.apache.sis.pending.geoapi.filter.SpatialOperatorName;
+import org.apache.sis.pending.geoapi.filter.DistanceOperatorName;
/**
@@@ -142,8 -139,44 +142,32 @@@ public abstract class DefaultFilterFact
return Features.DEFAULT;
}
+ /**
+ * Returns a factory operating on resource instances of the given class.
+ * The current implementation recognizes the following classes:
+ *
+ * <ul>
+ * <li>{@link Feature}: delegate to {@link #forFeatures()}.</li>
+ * </ul>
+ *
+ * More classes may be added in future versions.
+ *
+ * @param <R> compile-time value of the {@code type} argument.
+ * @param type type of resources that the factory shall accept.
+ * @return factory operating on resource instances of the given class.
+ * @since 1.5
+ */
+ @SuppressWarnings("unchecked")
- public static <R> Optional<FilterFactory<R, Object, Object>> forResources(final Class<R> type) {
- if (type.equals(Feature.class)) {
- return Optional.of((FilterFactory<R, Object, Object>) forFeatures());
++ public static <R> Optional<DefaultFilterFactory<R, Object, Object>> forResources(final Class<R> type) {
++ if (type.equals(AbstractFeature.class)) {
++ return Optional.of((DefaultFilterFactory<R, Object, Object>) forFeatures());
+ } else {
+ return Optional.empty();
+ }
+ }
+
/**
- * Describes the abilities of this factory. The description includes restrictions on
- * the available spatial operations, scalar operations, lists of supported functions,
- * and description of which geometry literals are understood.
- *
- * @return description of the abilities of this factory.
- */
- @Override
- public FilterCapabilities getCapabilities() {
- return new Capabilities(this); // Cheap to construct, no need to cache.
- }
-
- /**
- * A filter factory operating on {@link Feature} instances.
+ * A filter factory operating on {@link AbstractFeature} instances.
*
* @param <G> base class of geometry objects. The implementation-neutral type is GeoAPI {@link Geometry},
* but this factory allows the use of other implementations such as JTS
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
index 0000000000,3a49222670..86379fdf59
mode 000000,100644..100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/InvalidXPathException.java
@@@ -1,0 -1,81 +1,78 @@@
+ /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+ package org.apache.sis.filter;
+
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.filter.InvalidFilterValueException;
-
+
+ /**
+ * Exceptions thrown when the XPath in an expression is invalid or unsupported.
+ * Apache SIS currently supports only a small subset of XPath syntax, mostly paths of
+ * the form {@code "a/b/c"} (and not everywhere) and the {@code "Q{namespace}"} syntax.
+ *
+ * <h4>Relationship with standard libraries</h4>
+ * The standard Java libraries provides a {@link javax.xml.xpath.XPathException}.
+ * This {@code InvalidXPathException} differs in that it is an unchecked exception
+ * and is thrown in the context of operations with OGC filters and expressions.
+ * In some implementations, {@code InvalidXPathException} may have a {@code XPathException} as its cause.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.5
+ *
+ * @see javax.xml.xpath.XPathException
+ *
+ * @since 1.5
+ */
-public class InvalidXPathException extends InvalidFilterValueException {
++public class InvalidXPathException extends IllegalArgumentException {
+ /**
+ * Serial number for inter-operability with different versions.
+ */
+ private static final long serialVersionUID = 1654277877397802378L;
+
+ /**
+ * Creates an exception with no message.
+ */
+ public InvalidXPathException() {
+ super();
+ }
+
+ /**
+ * Creates an exception with the specified message.
+ *
+ * @param message the detail message, saved for later retrieval by the {@link #getMessage()} method.
+ */
+ public InvalidXPathException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Creates an exception with the specified cause.
+ *
+ * @param cause the cause, saved for later retrieval by the {@link #getCause()} method.
+ */
+ public InvalidXPathException(final Throwable cause) {
+ super(cause);
+ }
+
+ /**
+ * Creates an exception with the specified message and cause.
+ *
+ * @param message the detail message, saved for later retrieval by the {@link #getMessage()} method.
+ * @param cause the cause, saved for later retrieval by the {@link #getCause()} method.
+ */
+ public InvalidXPathException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+ }
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
index a502cbb779,a869d79ae4..0c9958f264
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
@@@ -27,14 -27,13 +27,12 @@@ import org.apache.sis.util.iso.Names
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.WeakValueHashMap;
import org.apache.sis.feature.privy.FeatureExpression;
+ import org.apache.sis.feature.privy.FeatureProjectionBuilder;
import org.apache.sis.filter.internal.Node;
- import org.apache.sis.feature.builder.FeatureTypeBuilder;
- import org.apache.sis.feature.builder.PropertyTypeBuilder;
import org.apache.sis.math.FunctionProperty;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.AttributeType;
-import org.opengis.filter.Expression;
+// Specific to the main branch:
- import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.feature.DefaultAttributeType;
/**
@@@ -154,16 -149,17 +152,17 @@@ abstract class LeafExpression<R,V> exte
}
/**
- * Provides the type of values returned by {@link #apply(Object)}
- * wrapped in an {@link DefaultAttributeType} named "Literal".
+ * Provides the type of values returned by {@link #apply(Object)}.
- * The returned item wraps an {@link AttributeType} named "Literal".
++ * The returned item wraps an {@code AttributeType} named "Literal".
+ * The attribute type is determined by the class of the {@linkplain #value}.
*
* @param addTo where to add the type of properties evaluated by the given expression.
- * @return builder of the added property.
+ * @return handler for the added property.
*/
@Override
- public PropertyTypeBuilder expectedType(DefaultFeatureType ignored, final FeatureTypeBuilder addTo) {
+ public FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
final Class<?> valueType = getValueClass();
- AttributeType<?> propertyType;
+ DefaultAttributeType<?> propertyType;
synchronized (TYPES) {
propertyType = TYPES.get(valueType);
if (propertyType == null) {
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java
index 9fc8e48551,06410e94c4..489c777fbd
--- 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
@@@ -29,15 -27,13 +27,14 @@@ import org.apache.sis.feature.privy.Fea
import org.apache.sis.filter.privy.XPath;
import org.apache.sis.util.resources.Errors;
-// 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.PropertyType;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.PropertyNotFoundException;
-import org.opengis.filter.ValueReference;
+// Specific to the main branch:
+import org.opengis.util.ScopedName;
+import org.apache.sis.feature.AbstractFeature;
+import org.apache.sis.feature.AbstractIdentifiedType;
- import org.apache.sis.feature.AbstractOperation;
+import org.apache.sis.feature.DefaultAttributeType;
+import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.pending.geoapi.filter.Name;
+import org.apache.sis.pending.geoapi.filter.ValueReference;
/**
@@@ -375,21 -366,27 +375,27 @@@ abstract class PropertyValue<V> extend
/**
* Provides the expected type of values produced by this expression when a feature of the given type is evaluated.
+ * The source feature type is specified indirectly by {@link FeatureProjectionBuilder#source()}.
*
- * @param valueType the type of features to be evaluated by the given expression.
- * @param addTo where to add the type of properties evaluated by the given expression.
+ * <h4>Handling of operations</h4>
+ * Properties that are operations are replaced by attributes where the operation result will be stored.
+ * An exception to this rule is the links such as {@code "sis:identifier"} and {@code "sis:geometry"},
+ * in which case the link operation is kept. It may force {@code FeatureProjectionBuilder} to add also
+ * the dependencies (targets) of the link.
+ *
+ * @param addTo where to add the type of properties evaluated by this expression.
* @return builder of the added property, or {@code null} if this method cannot add a property.
- * @throws PropertyNotFoundException if the property was not found in {@code addTo.source()}.
+ * @throws IllegalArgumentException if this method cannot determine the property type for the given feature type.
*/
@Override
- public PropertyTypeBuilder expectedType(final DefaultFeatureType valueType, final FeatureTypeBuilder addTo) {
+ public FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
- PropertyType type;
+ AbstractIdentifiedType type;
try {
- type = valueType.getProperty(name);
+ type = addTo.source().getProperty(name);
- } catch (PropertyNotFoundException e) {
+ } catch (IllegalArgumentException e) {
if (isVirtual) {
- // The property does not exist but may be defined on a yet unknown child type.
- return expectedType(addTo);
+ // The property does not exist but may be defined in a yet unknown child type.
+ return defaultType(addTo);
}
throw e;
}
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/FunctionWithSRID.java
index 40728be85a,2d751dd31e..82b5e59df7
--- 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
@@@ -30,10 -28,10 +28,9 @@@ import org.apache.sis.feature.privy.Fea
import org.apache.sis.util.privy.Constants;
import org.apache.sis.util.resources.Errors;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.filter.Literal;
-import org.opengis.filter.Expression;
-import org.opengis.filter.InvalidFilterValueException;
+// Specific to the main branch:
- import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.filter.Expression;
+import org.apache.sis.pending.geoapi.filter.Literal;
/**
diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/SpatialFunction.java
index f4f27cc967,b938087a8a..714288c7f8
--- 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
@@@ -32,9 -30,9 +30,8 @@@ import org.apache.sis.util.ArgumentChec
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.iso.Names;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.filter.Expression;
-import org.opengis.filter.InvalidFilterValueException;
+// Specific to the main branch:
+import org.apache.sis.filter.Expression;
- import org.apache.sis.feature.DefaultFeatureType;
/**
@@@ -180,33 -178,28 +177,28 @@@ abstract class SpatialFunction<R> exten
* <li>Otherwise an attribute is created with the return value specified by the operation.</li>
* </ul>
*
- * @param valueType the type of features on which to apply this expression.
- * @param addTo where to add the type of properties evaluated by this expression.
+ * @param addTo where to add the type of properties evaluated by this expression.
* @return builder of type resulting from expression evaluation (never null).
- * @throws IllegalArgumentException if the given feature type does not contain the expected properties,
- * @throws InvalidFilterValueException if the source feature type does not contain the expected properties,
++ * @throws IllegalArgumentException if the source feature type does not contain the expected properties,
* or if this method cannot determine the result type of the expression.
* It may be because that expression is backed by an unsupported implementation.
*/
@Override
- public PropertyTypeBuilder expectedType(final DefaultFeatureType valueType, final FeatureTypeBuilder addTo) {
- AttributeTypeBuilder<?> att;
- cases: if (operation.isGeometryInOut()) {
+ public FeatureProjectionBuilder.Item expectedType(final FeatureProjectionBuilder addTo) {
+ if (operation.isGeometryInOut()) {
final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(getParameters().get(0));
if (fex != null) {
- final PropertyTypeBuilder type = fex.expectedType(valueType, addTo);
- if (type instanceof AttributeTypeBuilder<?>) {
- att = (AttributeTypeBuilder<?>) type;
- final Geometries<?> library = Geometries.factory(att.getValueClass());
- if (library != null) {
- att = att.setValueClass(operation.getReturnType(library));
- break cases;
- }
+ final FeatureProjectionBuilder.Item item = fex.expectedType(addTo);
+ final boolean success = item.replaceValueClass((c) -> {
+ final Geometries<?> library = Geometries.factory(c);
+ return (library == null) ? null : operation.getReturnType(library);
+ });
+ if (success) {
+ return item;
}
}
- throw new InvalidFilterValueException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression));
+ throw new IllegalArgumentException(Resources.format(Resources.Keys.NotAGeometryAtFirstExpression));
- } else {
- att = addTo.addAttribute(getValueClass());
}
- return att.setName(getFunctionName());
+ return addTo.addComputedProperty(addTo.addAttribute(getValueClass()).setName(getFunctionName()), false);
}
}
diff --cc endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
index 7ff19f8852,4636952051..83dc035fcc
--- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java
@@@ -25,14 -25,14 +25,14 @@@ import java.sql.PreparedStatement
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.sis.metadata.sql.privy.SQLBuilder;
- import org.apache.sis.storage.base.FeatureProjection;
+ import org.apache.sis.feature.privy.FeatureProjection;
import org.apache.sis.util.collection.WeakValueHashMap;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.Feature;
-import org.opengis.filter.SortOrder;
-import org.opengis.filter.SortProperty;
-import org.opengis.filter.SortBy;
+// Specific to the main branch:
+import org.apache.sis.feature.AbstractFeature;
+import org.apache.sis.pending.geoapi.filter.SortOrder;
+import org.apache.sis.pending.geoapi.filter.SortProperty;
+import org.apache.sis.pending.geoapi.filter.SortBy;
/**
diff --cc endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java
index 4e81c0694d,2386a8fa35..963613ab67
--- 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
@@@ -41,13 -39,12 +39,12 @@@ import org.apache.sis.util.privy.String
import org.apache.sis.util.stream.DeferredStream;
import org.apache.sis.util.stream.PaginedStream;
import org.apache.sis.storage.DataStoreException;
- import org.apache.sis.storage.base.FeatureProjection;
+ import org.apache.sis.feature.privy.FeatureProjection;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.Feature;
-import org.opengis.filter.Filter;
-import org.opengis.filter.SortBy;
+// Specific to the main branch:
+import org.apache.sis.filter.Filter;
- import org.apache.sis.filter.Expression;
+import org.apache.sis.feature.AbstractFeature;
+import org.apache.sis.pending.geoapi.filter.SortBy;
/**
diff --cc endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
index 1af3574bf7,9b7981d788..7110ac76a7
--- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/InfoStatements.java
@@@ -57,9 -57,10 +57,10 @@@ import org.apache.sis.util.privy.Consta
import org.apache.sis.io.wkt.Convention;
import org.apache.sis.io.wkt.WKTFormat;
import org.apache.sis.io.wkt.Warnings;
+ import org.apache.sis.util.Workaround;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.metadata.Identifier;
+// Specific to the main branch:
+import org.opengis.referencing.ReferenceIdentifier;
/**
diff --cc endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
index 7d53b4d7d6,1a56e7149e..118a8f666c
--- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
+++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java
@@@ -43,11 -46,12 +46,10 @@@ import org.apache.sis.util.collection.T
import org.apache.sis.util.iso.DefaultNameSpace;
import org.apache.sis.util.resources.Errors;
-// 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.FeatureAssociationRole;
-import org.opengis.filter.InvalidFilterValueException;
+// Specific to the main branch:
- import org.apache.sis.filter.Expression;
+import org.apache.sis.feature.AbstractFeature;
+import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.feature.DefaultAssociationRole;
/**
@@@ -76,9 -80,16 +78,16 @@@ final class Table extends AbstractFeatu
* except synthetic attributes like "sis:identifier". The feature may also contain associations
* inferred from foreigner keys that are not immediately apparent in the table.
*
+ * <h4>Relationship with {@code FeatureSet.getType()}</h4>
+ * When this {@code Table} has been created by inspection of the database metadata, this feature type
+ * is the value shown to users. But when this {@code Table} has been created from a query, this value
+ * is not directly shown to user because it may contain additional properties for the dependencies of
+ * operations. In the latter case, this table is hidden behind {@link FeatureIterator} and the type
+ * seen by user is provided by {@link org.apache.sis.storage.FeatureSubset#resultType}.
+ *
* @see #getType()
*/
- final FeatureType featureType;
+ final DefaultFeatureType featureType;
/**
* The SQL query to execute for fetching data, or {@code null} for querying the table identified by {@link #name}.
@@@ -193,13 -204,13 +202,13 @@@
* Creates a new table as a projection (subset of columns) of the given table.
* The columns to retain, potentially under different names, are specified in {@code projection}.
* The projection may also contain complex expressions that cannot be handled by this constructor.
- * Such expressions are stored in the {@code unhandled} map.
+ * Such expressions have their indexes stored in the {@code unhandled} set.
*
- * @param table the source table.
+ * @param source the source table.
* @param projection description of the columns to keep.
* @param reusedNames an initially empty set where to store the names of attributes that are not renamed.
- * @param unhandled an initially empty map where to add expressions that are not handled by the new table.
+ * @param unhandled where to set the bits for indexes of expressions that are not handled by the new table.
- * @throws InvalidFilterValueException if there is an error in the declaration of property values.
+ * @throws IllegalArgumentException if there is an error in the declaration of property values.
*/
@SuppressWarnings("LocalVariableHidesMemberVariable")
Table(final Table source,
@@@ -384,9 -395,11 +393,11 @@@
/**
* Returns the feature type inferred from the database structure analysis.
+ * Note that this type may not be the type shown to user if this table is
+ * created behind a {@link org.apache.sis.storage.FeatureSubset}.
*/
@Override
- public final FeatureType getType() {
+ public final DefaultFeatureType getType() {
return featureType;
}
diff --cc endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
index fe55cf6f5c,62b25931c2..6b88e592f4
--- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
+++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
@@@ -439,6 -445,30 +441,30 @@@ public final class SQLStoreTest extend
assertEquals("FeatureStream[table=“Countries”, predicates=“SQL”]", executionMode);
}
+ /**
+ * Requests a new set of features with a subset containing the "sis:identifier" property.
+ * The difficulty is that no column named "sis:column" exists. The property is a link to
+ * the "code" attribute, which is intentionally omitted in the projection (in SQL sense)
+ * in order to test the capability to follow dependencies.
+ *
+ * @param dataset the store on which to query the features.
+ * @throws DataStoreException if an error occurred during query execution.
+ */
+ private void verifyLinkInProjection(final SimpleFeatureStore dataset) throws Exception {
+ final var query = new FeatureQuery();
+ query.setProjection("sis:identifier", "native_name");
+ final FeatureSet countries = dataset.findResource("Countries").subset(query);
+ List<Object> names;
- try (Stream<Feature> features = countries.features(false)) {
++ try (Stream<AbstractFeature> features = countries.features(false)) {
+ names = features.map(f -> f.getPropertyValue("sis:identifier")).toList();
+ }
+ assertSetEquals(List.of("CAN", "FRA", "JPN"), names);
- try (Stream<Feature> features = countries.features(false)) {
++ try (Stream<AbstractFeature> features = countries.features(false)) {
+ names = features.map(f -> f.getPropertyValue("native_name")).toList();
+ }
+ assertSetEquals(List.of("Canada", "France", "日本"), names);
+ }
+
/**
* Checks that operations stacked on feature stream are well executed.
* This test focuses on mapping and peeking actions overloaded by SQL streams.
diff --cc endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java
index 5f2d84d995,80dec5de00..7add475ead
--- 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
@@@ -40,15 -45,20 +45,16 @@@ import org.apache.sis.util.CharSequence
import org.apache.sis.util.Emptiable;
import org.apache.sis.util.iso.Names;
-// Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.Attribute;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.Operation;
-import org.opengis.filter.FilterFactory;
-import org.opengis.filter.Filter;
-import org.opengis.filter.Expression;
-import org.opengis.filter.InvalidFilterValueException;
-import org.opengis.filter.Literal;
-import org.opengis.filter.ValueReference;
-import org.opengis.filter.SortBy;
-import org.opengis.filter.SortProperty;
+// Specific to the main branch:
+import org.apache.sis.feature.AbstractFeature;
+import org.apache.sis.feature.AbstractAttribute;
++import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.filter.Filter;
+import org.apache.sis.filter.Expression;
+import org.apache.sis.pending.geoapi.filter.Literal;
+import org.apache.sis.pending.geoapi.filter.ValueReference;
+import org.apache.sis.pending.geoapi.filter.SortBy;
+import org.apache.sis.pending.geoapi.filter.SortProperty;
/**
@@@ -387,8 -395,9 +393,9 @@@ public class FeatureQuery extends Quer
*/
@SafeVarargs
@SuppressWarnings("varargs")
- public final void setSortBy(final SortProperty<Feature>... properties) {
+ final void setSortBy(final SortProperty<AbstractFeature>... properties) {
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
- SortBy<Feature> sortBy = null;
+ SortBy<AbstractFeature> sortBy = null;
if (properties != null) {
sortBy = SortByComparator.create(properties);
}
@@@ -609,6 -614,25 +616,25 @@@
this.alias = alias;
}
+ /**
+ * Adds this named expression as a property into the given builder.
+ *
+ * @param builder the builder where to add the property.
+ * @return whether the property has been successfully added.
+ */
+ final boolean addTo(final FeatureProjectionBuilder builder) {
- final FeatureExpression<? super Feature, ?> fex = FeatureExpression.castOrCopy(expression);
++ final FeatureExpression<? super AbstractFeature, ?> fex = FeatureExpression.castOrCopy(expression);
+ if (fex != null) {
+ final FeatureProjectionBuilder.Item item = fex.expectedType(builder);
+ if (item != null) {
+ item.setName(alias); // Need to be invoked aven if the alias is null.
+ item.setValueGetter(expression, type == ProjectionType.STORED);
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Returns a hash code value for this column.
*
@@@ -709,6 -733,36 +735,36 @@@
return xpaths;
}
+ /**
+ * Creates the projection (in <abbr>SQL</abbr> sense) of the given feature type.
+ * If some expressions have no name, default names are computed as below:
+ *
+ * <ul>
+ * <li>If the expression is an instance of {@link ValueReference}, the name of the
+ * property referenced by the {@linkplain ValueReference#getXPath() XPath}.</li>
+ * <li>Otherwise the localized string "Unnamed #1" with increasing numbers.</li>
+ * </ul>
+ *
+ * @param sourceType the feature type to project.
+ * @param locale locale for error messages, or {@code null} for the default locale.
+ */
- final Optional<FeatureProjection> project(final FeatureType sourceType, final Locale locale) {
++ final Optional<FeatureProjection> project(final DefaultFeatureType sourceType, final Locale locale) {
+ if (projection == null) {
+ return Optional.empty();
+ }
+ final var builder = new FeatureProjectionBuilder(sourceType, locale);
+ for (int column = 0; column < projection.length; column++) {
+ final NamedExpression item = projection[column];
+ if (!item.addTo(builder)) {
+ final var name = item.expression.getFunctionName().toInternationalString();
- throw new InvalidFilterValueException(Resources.forLocale(locale)
++ throw new IllegalArgumentException(Resources.forLocale(locale)
+ .getString(Resources.Keys.InvalidExpression_2, column, name));
+ }
+ }
+ builder.setName(sourceType.getName());
+ return builder.project();
+ }
+
/**
* Applies this query on the given feature set.
* This method is invoked by the default implementation of {@link FeatureSet#subset(Query)}.
diff --cc endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
index 88f140538c,c4d4e792a7..9230ef5e02
--- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
+++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java
@@@ -91,12 -91,12 +91,12 @@@ final class FeatureSubset extends Abstr
* Returns a description of properties that are common to all features in this dataset.
*/
@Override
- public synchronized FeatureType getType() throws DataStoreException {
+ public synchronized DefaultFeatureType getType() throws DataStoreException {
if (resultType == null) {
- final FeatureType type = source.getType();
+ final DefaultFeatureType type = source.getType();
try {
- projection = FeatureProjection.create(type, query.getProjection());
- resultType = (projection != null) ? projection.featureType : type;
+ projection = query.project(type, listeners.getLocale()).orElse(null);
+ resultType = (projection != null) ? projection.typeRequested : type;
} catch (IllegalArgumentException e) {
throw new DataStoreContentException(Resources.forLocale(listeners.getLocale())
.getString(Resources.Keys.CanNotDeriveTypeFromFeature_1, type.getName()), e);