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 <martin.desruisse...@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);