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 ad3d4f6c2f5a3fb27114d52558eecffba8fea59a Merge: 09335964ca 0704b92b5e Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Tue Apr 1 19:28:52 2025 +0200 Merge branch 'geoapi-3.1'. https://issues.apache.org/jira/browse/SIS-608 .../org/apache/sis/feature/AbstractOperation.java | 18 + .../org/apache/sis/feature/EnvelopeOperation.java | 3 +- .../apache/sis/feature/ExpressionOperation.java | 3 +- .../sis/feature/GroupAsPolylineOperation.java | 3 +- .../main/org/apache/sis/feature/LinkOperation.java | 3 +- .../apache/sis/feature/StringJoinOperation.java | 3 +- .../org/apache/sis/feature/internal/Resources.java | 5 + .../sis/feature/internal/Resources.properties | 1 + .../sis/feature/internal/Resources_fr.properties | 1 + .../sis/feature/privy/AttributeConvention.java | 6 + .../apache/sis/feature/privy/FeatureUtilities.java | 98 ------ .../apache/sis/filter/BinaryGeometryFilter.java | 2 +- .../main/org/apache/sis/filter/LeafExpression.java | 2 +- .../main/org/apache/sis/filter/internal/Node.java | 24 +- .../sis/filter/privy/ListingPropertyVisitor.java | 134 ++++++++ .../org/apache/sis/filter/privy/WarningEvent.java | 127 +++++++ .../geometry/wrapper/jts/GeometryTransform.java | 10 +- .../org/apache/sis/geometry/wrapper/jts/JTS.java | 20 +- .../main/org/apache/sis/image/ImageProcessor.java | 4 +- .../sis/geometry/wrapper/esri/FactoryTest.java | 8 +- .../apache/sis/geometry/wrapper/jts/JTSTest.java | 2 +- .../org/apache/sis/metadata/sql/privy/Dialect.java | 52 ++- .../apache/sis/metadata/sql/privy/SQLBuilder.java | 116 +++++-- .../sis/metadata/sql/privy/SQLUtilities.java | 43 ++- .../org/apache/sis/metadata/sql/privy/Syntax.java | 70 +++- .../org/apache/sis/temporal/LenientDateFormat.java | 6 +- .../main/org/apache/sis/temporal/TimeMethods.java | 6 +- .../sis/metadata/sql/privy/SQLBuilderTest.java | 97 ++++++ .../sis/metadata/sql/privy/SQLUtilitiesTest.java | 13 +- .../main/org/apache/sis/openoffice/CalcAddins.java | 2 +- .../coverage/MultiResolutionCoverageLoader.java | 2 +- .../apache/sis/io/wkt/GeodeticObjectParser.java | 21 +- .../sis/referencing/operation/matrix/Matrices.java | 62 +++- .../referencing/operation/matrix/MatricesTest.java | 16 + .../apache/sis/storage/landsat/MetadataReader.java | 16 +- .../geotiff/reader/GridGeometryBuilder.java | 13 +- .../sis/storage/geotiff/writer/GeoEncoder.java | 85 ++++- .../sis/storage/netcdf/base/GridMapping.java | 2 +- .../sis/storage/netcdf/ucar/DecoderWrapper.java | 3 +- .../main/module-info.java | 1 + .../org/apache/sis/storage/sql/DataAccess.java | 2 + .../main/org/apache/sis/storage/sql/SQLStore.java | 43 ++- .../org/apache/sis/storage/sql/duckdb/DuckDB.java | 88 +++++ .../{postgis => duckdb}/ExtendedClauseWriter.java | 12 +- .../sis/storage/sql/duckdb/package-info.java | 60 ++++ .../apache/sis/storage/sql/feature/Analyzer.java | 112 ++++-- .../org/apache/sis/storage/sql/feature/Column.java | 67 +++- .../apache/sis/storage/sql/feature/Database.java | 191 +++++++++-- .../sis/storage/sql/feature/FeatureAdapter.java | 8 +- .../sis/storage/sql/feature/FeatureAnalyzer.java | 12 +- .../sis/storage/sql/feature/FeatureIterator.java | 87 ++++- .../sis/storage/sql/feature/FeatureStream.java | 91 ++++- .../sis/storage/sql/feature/GeometryEncoding.java | 25 +- .../sis/storage/sql/feature/GeometryGetter.java | 103 ++++-- .../sis/storage/sql/feature/InfoStatements.java | 104 ++++-- .../sis/storage/sql/feature/QueryAnalyzer.java | 4 +- .../apache/sis/storage/sql/feature/Relation.java | 48 ++- .../apache/sis/storage/sql/feature/Resources.java | 15 + .../sis/storage/sql/feature/Resources.properties | 3 + .../storage/sql/feature/Resources_fr.properties | 5 +- .../sis/storage/sql/feature/SelectionClause.java | 249 +++++++++++++- .../storage/sql/feature/SelectionClauseWriter.java | 38 +- .../sis/storage/sql/feature/SpatialSchema.java | 73 +++- .../org/apache/sis/storage/sql/feature/Table.java | 147 ++++++-- .../sis/storage/sql/feature/TableAnalyzer.java | 61 ++-- .../sis/storage/sql/feature/TableReference.java | 2 +- .../storage/sql/postgis/ExtendedClauseWriter.java | 5 + .../sis/storage/sql/postgis/ExtendedInfo.java | 32 +- .../sis/storage/sql/postgis/ExtentEstimator.java | 2 +- .../apache/sis/storage/sql/postgis/Postgres.java | 22 +- .../org/apache/sis/storage/sql/SQLStoreTest.java | 22 +- .../storage/sql/feature/GeometryGetterTest.java | 3 +- .../sis/storage/sql/postgis/PostgresTest.java | 125 ++++++- .../sis/storage/sql/postgis/RasterReaderTest.java | 5 +- .../sis/storage/sql/postgis/RasterWriterTest.java | 4 +- .../sis/storage/sql/postgis/SpatialFeatures.sql | 8 + .../apache/sis/io/stream/InternalOptionKey.java | 2 +- .../main/org/apache/sis/storage/FeatureQuery.java | 237 +++++-------- .../main/org/apache/sis/storage/FeatureSubset.java | 37 +- .../org/apache/sis/storage/StorageConnector.java | 28 +- .../apache/sis/storage/base/FeatureProjection.java | 381 +++++++++++++++++++++ .../apache/sis/storage/base/MetadataBuilder.java | 114 ++++-- .../main/org/apache/sis/storage/csv/Store.java | 2 +- .../org/apache/sis/storage/image/FormatFilter.java | 4 +- .../org/apache/sis/storage/image/FormatFinder.java | 67 +--- .../apache/sis/storage/image/MultiImageStore.java | 2 +- .../apache/sis/storage/image/SingleImageStore.java | 2 +- .../apache/sis/storage/image/WorldFileStore.java | 31 +- .../apache/sis/storage/image/WritableStore.java | 2 +- .../org/apache/sis/storage/wkt/StoreFormat.java | 2 +- .../org/apache/sis/storage/FeatureQueryTest.java | 23 ++ .../sis/storage/image/SelfConsistencyTest.java | 2 +- .../sis/storage/image/WorldFileStoreTest.java | 13 +- .../services/org.apache.sis.util.ObjectConverter | 11 + .../org/apache/sis/converter/StringConverter.java | 2 +- .../main/org/apache/sis/setup/GeometryLibrary.java | 37 +- .../org/apache/sis/util/collection/WeakEntry.java | 2 +- .../apache/sis/util/collection/WeakHashSet.java | 1 + .../org/apache/sis/util/privy/CollectionsExt.java | 18 + .../org/apache/sis/util/resources/Vocabulary.java | 5 + .../sis/util/resources/Vocabulary.properties | 1 + .../sis/util/resources/Vocabulary_fr.properties | 1 + .../apache/sis/storage/geoheif/GeoHeifStore.java | 2 +- .../storage/shapefile/ListingPropertyVisitor.java | 79 ----- .../sis/storage/shapefile/ShapefileStore.java | 7 +- .../apache/sis/storage/shapefile/dbf/DBFField.java | 11 +- .../apache/sis/gui/coverage/CoverageCanvas.java | 68 +++- .../gui/coverage/MultiResolutionImageLoader.java | 2 +- .../main/org/apache/sis/gui/map/MapCanvas.java | 6 +- .../main/org/apache/sis/gui/map/package-info.java | 2 +- .../org/apache/sis/storage/gdal/FieldAccessor.java | 3 +- .../org/apache/sis/storage/gdal/GDALStore.java | 4 +- 112 files changed, 3190 insertions(+), 1002 deletions(-) diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java index 24efcf6322,a498244380..dc0a3b7d8d --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractOperation.java @@@ -30,7 -31,19 +31,9 @@@ import org.opengis.parameter.GeneralPar import org.opengis.parameter.ParameterDescriptorGroup; import org.opengis.parameter.ParameterValueGroup; import org.apache.sis.util.Classes; + import org.apache.sis.metadata.iso.citation.Citations; + import org.apache.sis.parameter.DefaultParameterDescriptorGroup; -// Specific to the geoapi-3.1 and geoapi-4.0 branches: -import org.opengis.feature.Attribute; -import org.opengis.feature.AttributeType; -import org.opengis.feature.Feature; -import org.opengis.feature.FeatureAssociation; -import org.opengis.feature.FeatureOperationException; -import org.opengis.feature.IdentifiedType; -import org.opengis.feature.Operation; -import org.opengis.feature.Property; - /** * Describes the behaviour of a feature type as a function or a method. diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java index f5efb4ae70,9bf239939c..02a3a25491 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/LinkOperation.java @@@ -21,9 -21,14 +21,8 @@@ import java.util.Map import java.io.IOException; import org.opengis.parameter.ParameterValueGroup; import org.opengis.parameter.ParameterDescriptorGroup; - import org.apache.sis.feature.privy.FeatureUtilities; 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.IdentifiedType; -import org.opengis.feature.Property; -import org.opengis.feature.PropertyType; - /** * A link operation, which is like a redirection or an alias. diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java index caecc6ea92,8afbdfd5d0..2a7b94368f --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java @@@ -179,11 -183,12 +181,12 @@@ public abstract class Node implements S final Geometries<G> library, final Expression<R,?> expression) { if (expression instanceof GeometryConverter<?,?>) { - if (library.equals(((GeometryConverter<?,?>) expression).library)) { + final Geometries<?> other = ((GeometryConverter<?,?>) expression).library; + if (library.equals(other)) { return (GeometryConverter<R,G>) expression; - } else { - throw new IllegalArgumentException(); // TODO: provide a message. } - throw new InvalidFilterValueException(Resources.format( ++ throw new IllegalArgumentException(Resources.format( + Resources.Keys.MixedGeometryImplementation_2, library.library, other.library)); } return new GeometryConverter<>(library, expression); } diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/ListingPropertyVisitor.java index 0000000000,4b9e664a31..719d2f7cb7 mode 000000,100644..100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/ListingPropertyVisitor.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/ListingPropertyVisitor.java @@@ -1,0 -1,137 +1,134 @@@ + /* + * 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.privy; + + import java.util.HashSet; + import java.util.Set; + -// Specific to the geoapi-3.1 and geoapi-4.0 branches: -import org.opengis.util.CodeList; -import org.opengis.filter.BetweenComparisonOperator; -import org.opengis.filter.Filter; -import org.opengis.filter.Expression; -import org.opengis.filter.ValueReference; -import org.opengis.filter.ComparisonOperatorName; -import org.opengis.filter.LikeOperator; -import org.opengis.filter.LogicalOperator; ++// Specific to the main branch: ++import org.apache.sis.filter.Filter; ++import org.apache.sis.filter.Expression; ++import org.apache.sis.pending.geoapi.filter.BetweenComparisonOperator; ++import org.apache.sis.pending.geoapi.filter.ValueReference; ++import org.apache.sis.pending.geoapi.filter.ComparisonOperatorName; ++import org.apache.sis.pending.geoapi.filter.LogicalOperator; + + + /** + * A collector of all attributes required by a filter or an expression. + * This visitor collects the XPaths of all {@link ValueReference} found. + * + * @author Johann Sorel (Geomatys) + * @author Martin Desruisseaux (Geomatys) + */ + public final class ListingPropertyVisitor extends Visitor<Object, Set<String>> { + /** + * The unique instance of this visitor. + */ + private static final ListingPropertyVisitor INSTANCE = new ListingPropertyVisitor(); + + /** + * Creates the unique instance of this visitor. + */ + private ListingPropertyVisitor() { + setLogicalHandlers((f, names) -> { + final var filter = (LogicalOperator<Object>) f; + for (Filter<Object> child : filter.getOperands()) { + visit(child, names); + } + }); + setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_BETWEEN), (f, names) -> { + final var filter = (BetweenComparisonOperator<Object>) f; + visit(filter.getExpression(), names); + visit(filter.getLowerBoundary(), names); + visit(filter.getUpperBoundary(), names); + }); + setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_LIKE), (f, names) -> { - final var filter = (LikeOperator<Object>) f; - visit(filter.getExpressions().get(0), names); ++ visit(f.getExpressions().get(0), names); + }); + setExpressionHandler(FunctionNames.ValueReference, (e, names) -> { + final var expression = (ValueReference<Object,?>) e; + names.add(expression.getXPath()); + }); + } + + /** + * Visits all operands of the given filter for listing all value references. + * + * @param type the filter type (may be {@code null}). + * @param filter the filter (may be {@code null}). + * @param xpaths where to add the XPaths. + */ + @Override - protected void typeNotFound(final CodeList<?> type, final Filter<Object> filter, final Set<String> xpaths) { ++ protected void typeNotFound(final Enum<?> type, final Filter<Object> filter, final Set<String> xpaths) { + for (final var f : filter.getExpressions()) { + visit(f, xpaths); + } + } + + /** + * Visits all parameters of the given expression for listing all value references. + * + * @param type the expression type (may be {@code null}). + * @param expression the expression (may be {@code null}). + * @param xpaths where to add the XPaths. + */ + @Override + protected void typeNotFound(final String type, final Expression<Object, ?> expression, final Set<String> xpaths) { + for (final var p : expression.getParameters()) { + visit(p, xpaths); + } + } + + /** + * Returns all XPaths used, directly or indirectly, by the given filter. + * The elements in the set are in no particular order. + * + * @param filter the filter for which to get the XPaths. May be {@code null}. + * @param xpaths a pre-allocated collection where to add the XPaths, or {@code null} if none. + * @return the given collection, or a new one if it was {@code null}, with XPaths added. + */ + @SuppressWarnings("unchecked") + public static Set<String> xpaths(final Filter<?> filter, Set<String> xpaths) { + if (xpaths == null) { + xpaths = new HashSet<>(); + } + if (filter != null) { + INSTANCE.visit((Filter) filter, xpaths); + } + return xpaths; + } + + /** + * Returns all XPaths used, directly or indirectly, by the given expression. + * The elements in the set are in no particular order. + * + * @param expression the expression for which to get the XPaths. May be {@code null}. + * @param xpaths a pre-allocated collection where to add the XPaths, or {@code null} if none. + * @return the given collection, or a new one if it was {@code null}, with XPaths added. + */ + @SuppressWarnings("unchecked") + public static Set<String> xpaths(final Expression<?,?> expression, Set<String> xpaths) { + if (xpaths == null) { + xpaths = new HashSet<>(); + } + if (expression != null) { + INSTANCE.visit((Expression) expression, xpaths); + } + return xpaths; + } + } diff --cc endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/WarningEvent.java index 0000000000,09a6f891d5..a8e83940c6 mode 000000,100644..100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/WarningEvent.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/WarningEvent.java @@@ -1,0 -1,128 +1,127 @@@ + /* + * 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.privy; + + import java.util.Optional; + import java.util.function.Consumer; -import org.opengis.util.CodeList; + import org.opengis.util.ScopedName; + import org.apache.sis.filter.internal.Node; + -// Specific to the geoapi-3.1 and geoapi-4.0 branches: -import org.opengis.filter.Filter; -import org.opengis.filter.Expression; ++// Specific to the main branch: ++import org.apache.sis.filter.Filter; ++import org.apache.sis.filter.Expression; + + + /** + * A warning emitted during operations on filters or expressions. + * This class is a first draft that may move to public API in a future version. + * + * @author Martin Desruisseaux (Geomatys) + * + * @see <a href="https://issues.apache.org/jira/browse/SIS-460">SIS-460</a> + */ + public final class WarningEvent { + /** + * Where to send the warning. If the value is {@code null}, + * then the warning will be logged to a default logger. + */ + public static final ThreadLocal<Consumer<WarningEvent>> LISTENER = new ThreadLocal<>(); + + /** + * The filter or expression that produced this warning. + */ + private final Node source; + + /** + * The exception that occurred. + */ + public final Exception exception; + + /** + * Creates a new warning. + * + * @param source the filter or expression that produced this warning. + * @param exception the exception that occurred. + */ + public WarningEvent(final Node source, final Exception exception) { + this.source = source; + this.exception = exception; + } + + /** + * If the source is a filter, returns the operator type. + * Otherwise, returns an empty value. + * + * @return the operator type if the source is a filter. + */ - public Optional<CodeList<?>> getOperatorType() { ++ public Optional<Enum<?>> getOperatorType() { + if (source instanceof Filter<?>) { + return Optional.of(((Filter<?>) source).getOperatorType()); + } + return Optional.empty(); + } + + /** + * If the source is an expression, returns the function name. + * Otherwise, returns an empty value. + * + * @return the function name if the source is an expression. + */ + public Optional<ScopedName> getFunctionName() { + if (source instanceof Expression<?,?>) { + return Optional.of(((Expression<?,?>) source).getFunctionName()); + } + return Optional.empty(); + } + + /** + * If the source is an expression with at least one parameter of the given type, returns that parameter. + * If there is many parameter assignable to the given type, then the first occurrence is returned. + * The {@code type} argument is typically {@code Literal.class} or {@code ValueReference.class}. + * + * @param type the desired type of the parameter to return. + * @return the first parameter of the given type, or empty if none. + */ + @SuppressWarnings("unchecked") + public <P extends Expression<?,?>> Optional<P> getParameter(final Class<P> type) { + if (source instanceof Filter<?>) { + for (Expression<?,?> parameter : ((Filter<?>) source).getExpressions()) { + if (type.isInstance(parameter)) { + return Optional.of((P) parameter); + } + } + } + if (source instanceof Expression<?,?>) { + for (Expression<?,?> parameter : ((Expression<?,?>) source).getParameters()) { + if (type.isInstance(parameter)) { + return Optional.of((P) parameter); + } + } + } + return Optional.empty(); + } + + /** + * Returns a string representation of the warning for debugging purposes. + * + * @return a string representation of the warning. + */ + @Override + public String toString() { + return source.getDisplayName() + ": " + exception.toString(); + } + } diff --cc endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/ExtendedClauseWriter.java index 140192163f,a6232bda80..41956aef67 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/ExtendedClauseWriter.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/duckdb/ExtendedClauseWriter.java @@@ -14,12 -14,10 +14,12 @@@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.sis.storage.sql.postgis; + package org.apache.sis.storage.sql.duckdb; import org.apache.sis.storage.sql.feature.SelectionClauseWriter; -import org.opengis.filter.SpatialOperatorName; + +// Specific to the main branch: +import org.apache.sis.pending.geoapi.filter.SpatialOperatorName; /** diff --cc endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureIterator.java index 30b9a8dfce,428b947eac..7ff19f8852 --- 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 @@@ -26,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.InternalDataStoreException; + import org.apache.sis.storage.base.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; /** @@@ -110,18 -132,29 +132,29 @@@ final class FeatureIterator implements * @param sort the {@code ORDER BY} clauses, or {@code null} if none. * @param offset number of rows to skip in underlying SQL query, or ≤ 0 for none. * @param count maximum number of rows to return, or ≤ 0 for no limit. + * @param projection additional properties to compute, or {@code null} if none. */ - FeatureIterator(final Table table, final Connection connection, - final boolean distinct, final String filter, final SortBy<? super AbstractFeature> sort, - final long offset, final long count) - throws SQLException, InternalDataStoreException + FeatureIterator(final Table table, + final Connection connection, + final boolean distinct, + final SelectionClause selection, - final SortBy<? super Feature> sort, ++ final SortBy<? super AbstractFeature> sort, + final long offset, + final long count, + final FeatureProjection projection) + throws Exception { adapter = table.adapter(connection); - spatialInformation = table.database.getSpatialSchema().isPresent() - ? table.database.createInfoStatements(connection) : null; - String sql = adapter.sql; + String sql = adapter.sql; // Will be completed below with `WHERE` clause if needed. + + if (table.database.getSpatialSchema().isPresent()) { + spatialInformation = table.database.createInfoStatements(connection); + } else { + spatialInformation = null; + } + final String filter = (selection != null) ? selection.query(connection, spatialInformation) : null; if (distinct || filter != null || sort != null || offset > 0 || count > 0) { - final SQLBuilder builder = new SQLBuilder(table.database).append(sql); + final var builder = new SQLBuilder(table.database).append(sql); if (distinct) { builder.insertDistinctAfterSelect(); } @@@ -242,9 -284,9 +284,9 @@@ * @param all {@code true} for reading all remaining feature instances, or {@code false} for only the next one. * @return {@code true} if we have read an instance and {@code all} is {@code false} (so there is maybe other instances). */ - private boolean fetch(final Consumer<? super Feature> action, final boolean all) throws Exception { + private boolean fetch(final Consumer<? super AbstractFeature> action, final boolean all) throws Exception { while (result.next()) { - final AbstractFeature feature = adapter.createFeature(spatialInformation, result); - Feature feature = adapter.createFeature(spatialInformation, result); ++ AbstractFeature feature = adapter.createFeature(spatialInformation, result); for (int i=0; i < dependencies.length; i++) { WeakValueHashMap<?,Object> instances = null; Object key = null, value = null; @@@ -292,8 -347,8 +347,8 @@@ * @param owner if the features to fetch are components of another feature, that container feature instance. * @return the feature as a singleton {@code Feature} or as a {@code Collection<Feature>}. */ - private Object fetchReferenced(final Feature owner) throws Exception { - final var features = new ArrayList<Feature>(); + private Object fetchReferenced(final AbstractFeature owner) throws Exception { - final List<AbstractFeature> features = new ArrayList<>(); ++ final var features = new ArrayList<AbstractFeature>(); try (ResultSet r = statement.executeQuery()) { result = r; fetch(features::add, true); diff --cc endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/FeatureStream.java index 1ad73ac900,c2a226c459..c1dff382eb --- 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 @@@ -35,12 -39,15 +39,15 @@@ 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.filter.privy.SortByComparator; + import org.apache.sis.filter.privy.WarningEvent; import org.apache.sis.storage.DataStoreException; + import org.apache.sis.storage.base.FeatureProjection; -// Specific to the geoapi-3.1 and geoapi-4.0 branches: -import org.opengis.feature.Feature; -import org.opengis.filter.Expression; -import org.opengis.filter.Filter; -import org.opengis.filter.SortBy; +// 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; /** @@@ -177,17 -192,22 +192,22 @@@ final class FeatureStream extends Defer * if we have a "F₀ AND F₁ AND F₂" chain, it is possible to have some Fₙ as SQL statements and * other Fₙ executed in Java code. */ - final Optimization optimization = new Optimization(); - optimization.setFeatureType(table.featureType); - Stream<Feature> stream = this; + Stream<AbstractFeature> stream = this; - for (final Filter<? super AbstractFeature> filter : optimization.applyAndDecompose((Filter<? super AbstractFeature>) predicate)) { - if (filter == Filter.include()) continue; - if (filter == Filter.exclude()) return empty(); - if (!selection.tryAppend(filterToSQL, filter)) { - // Delegate to Java code all filters that we cannot translate to SQL statement. - stream = super.filter(filter); - hasPredicates = true; + try { + WarningEvent.LISTENER.set(selection); + final var optimization = new Optimization(); + optimization.setFeatureType(table.featureType); - for (final var filter : optimization.applyAndDecompose((Filter<? super Feature>) predicate)) { ++ for (final var filter : optimization.applyAndDecompose((Filter<? super AbstractFeature>) predicate)) { + if (filter == Filter.include()) continue; + if (filter == Filter.exclude()) return empty(); + if (!selection.tryAppend(filterToSQL, filter)) { + // Delegate to Java code all filters that we cannot translate to SQL statement. + stream = super.filter(filter); + hasPredicates = true; + } } + } finally { + WarningEvent.LISTENER.remove(); } return stream; } @@@ -289,7 -309,12 +309,12 @@@ * be optimized. */ @Override + @SuppressWarnings("unchecked") - public <R> Stream<R> map(final Function<? super Feature, ? extends R> mapper) { + public <R> Stream<R> map(final Function<? super AbstractFeature, ? extends R> mapper) { + if (projection == null && mapper instanceof FeatureProjection) { + projection = (FeatureProjection) mapper; + return (Stream) this; + } return new PaginedStream<>(super.map(mapper), this); } @@@ -393,11 -421,35 +421,35 @@@ * @throws SQLException if an error occurs while executing the SQL statement. */ @Override - protected Spliterator<Feature> createSourceIterator() throws Exception { + protected Spliterator<AbstractFeature> createSourceIterator() throws Exception { - final String filter = (selection != null && !selection.isEmpty()) ? selection.toString() : null; - selection = null; // Let the garbage collector do its work. - - lock(table.database.transactionLocks); + Table projected = table; + FeatureProjection completion = null; + if (projection != null) { - final var unhandled = new LinkedHashMap<String, Expression<? super Feature, ?>>(); ++ final var unhandled = new LinkedHashMap<String, Expression<? super AbstractFeature, ?>>(); + final var reusedNames = new HashSet<String>(); + projected = new Table(projected, projection, reusedNames, unhandled); + if (!unhandled.isEmpty()) { + /* + * Some properties may not be handled by the projection. Check if the projected feature contains + * enough information for computing missing properties without the need for the original feature. + */ + Set<String> references = null; + for (var expression : unhandled.values()) { + references = ListingPropertyVisitor.xpaths(expression, references); + } + if (references == null || reusedNames.containsAll(references)) { + completion = new FeatureProjection(projected.featureType, unhandled); + } else { + /* + * Cannot use `projected` because some expressions need properties + * available only in the original features. + */ + projected = table; + completion = projection; + } + } + } + lock(projected.database.transactionLocks); final Connection connection = getConnection(); setCloseHandler(connection); // Executed only if `FeatureIterator` creation fails, discarded later otherwise. makeReadOnly(connection); diff --cc endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java index 9fc13b9955,72e55881ba..a5e5c9dbac --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java @@@ -16,19 -16,34 +16,33 @@@ */ package org.apache.sis.storage.sql.feature; + import java.util.Map; + import java.util.AbstractMap; + import java.util.List; + import java.util.ArrayList; + import java.util.Optional; + import java.util.logging.Level; + import java.util.logging.LogRecord; + import java.util.function.Consumer; + import java.sql.Connection; -import org.opengis.util.CodeList; import org.opengis.geometry.Envelope; - import org.opengis.geometry.Geometry; import org.opengis.metadata.extent.GeographicBoundingBox; + import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.geometry.WraparoundMethod; import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.geometry.wrapper.GeometryWrapper; import org.apache.sis.metadata.sql.privy.SQLBuilder; + import org.apache.sis.filter.privy.WarningEvent; + import org.apache.sis.storage.FeatureSet; + import org.apache.sis.system.Modules; + import org.apache.sis.util.Utilities; + import org.apache.sis.util.Workaround; -// 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.ValueReference; +// Specific to the main branch: +import org.apache.sis.feature.AbstractFeature; +import org.apache.sis.filter.Filter; +import org.apache.sis.pending.geoapi.filter.ValueReference; /** @@@ -60,6 -123,39 +122,39 @@@ public final class SelectionClause exte SelectionClause(final Table table) { super(table.database); this.table = table; + parameters = new ArrayList<>(); + } + + /** + * Clears any <abbr>CRS</abbr> that was configured by {@code acceptColumnCRS(…)}. + * This method should be invoked after the caller finished to write a spatial function. + */ + final void clearColumnCRS() { + columnCRS = null; + } + + /** + * If the referenced column is a geometry or raster column, remembers its default coordinate reference system. + * This method can be invoked before {@link #appendLiteral(Object)} in order to set a default <abbr>CRS</abbr> + * for literals that do not declare themselves their <abbr>CRS</abbr>. + * + * @param ref reference to a property to insert in SQL statement. + * @return whether the caller needs to stop the check of operands. + * + * @see #REPLACE_UNSPECIFIED_CRS + */ - final boolean acceptColumnCRS(final ValueReference<Feature,?> ref) { ++ final boolean acceptColumnCRS(final ValueReference<AbstractFeature,?> ref) { + final Column c = table.getColumn(ref.getXPath()); + if (c != null && c.getGeometryType().isPresent()) { + final Optional<CoordinateReferenceSystem> crs = c.getDefaultCRS(); + if (columnCRS == null) { + columnCRS = crs; // May be empty, which is not the same as null for this class. + } else if (!Utilities.equalsIgnoreMetadata(columnCRS.orElse(null), crs.orElse(null))) { + clearColumnCRS(); + return true; + } + } + return false; } /** @@@ -170,4 -308,90 +307,90 @@@ } return true; } + + /** + * Returns whether an error occurred while writing the <abbr>SQL</abbr> statement. + * If this method returns {@code true}, then the caller should truncate the SQL to + * the last length which was known to be valid and fallback on Java code for the rest. + */ + final boolean isInvalid() { + return isInvalid; + } + + /** + * Declares the SQL as invalid. It does not means that the whole SQL needs to be discarded. + * The SQL may be truncated to the last point where it was considered valid. + */ + final void invalidate() { + isInvalid = true; + } + + /** + * Returns the localized resources for warnings and error messages. + */ + private Resources resources() { + return Resources.forLocale(table.database.listeners.getLocale()); + } + + /** + * Sets the logger, class and method names of the given record, then logs it. + * This method declares {@link FeatureSet#features(boolean)} as the public source of the log. + * + * @param record the record to configure and log. + */ + private void log(final LogRecord record) { + record.setSourceClassName(FeatureSet.class.getName()); + record.setSourceMethodName("features"); + record.setLoggerName(Modules.SQL); + table.database.listeners.warning(record); + } + + /** + * Invoked when a warning occurred during operations on filters or expressions. + * + * @param event the warning. + */ + @Override + public void accept(final WarningEvent event) { + final LogRecord record = resources().getLogRecord( + Level.WARNING, + Resources.Keys.IncompatibleLiteralCRS_2, - event.getOperatorType().map(CodeList::identifier).orElse("?"), ++ event.getOperatorType().map(Enum::name).orElse("?"), + event.getParameter(ValueReference.class).map(ValueReference<?,?>::getXPath).orElse("?")); + record.setThrown(event.exception); + log(record); + } + + /** + * Returns the <abbr>SQL</abbr> fragment built by this {@code SelectionClause}. + * This method completes the information that we deferred until a connection is established. + * + * @param spatialInformation a cache of statements for fetching spatial information, or {@code null}. + * @return the <abbr>SQL</abbr> fragment, or {@code null} if there is no {@code WHERE} clause to add. + * @throws Exception if an SQL error, parsing error or other error occurred. + */ + final String query(final Connection connection, InfoStatements spatialInformation) throws Exception { + if (isEmpty()) { + return null; + } + boolean close = false; + for (int i = parameters.size(); --i >= 0;) { + if (spatialInformation == null) { + spatialInformation = table.database.createInfoStatements(connection); + close = true; + } + final var entry = parameters.get(i); + final int index = entry.getKey(); + final int srid = spatialInformation.findSRID(entry.getValue()); + buffer.replace(index, index + 1, Integer.toString(srid)); + } + if (close) { + /* + * We could put this in a `finally` block, but this method is already invoked + * in a context where the caller will close the connection in case of failure. + */ + spatialInformation.close(); + } + return buffer.toString(); + } } diff --cc endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java index c7d5e98412,a9e96dd22c..68a464221f --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClauseWriter.java @@@ -228,9 -229,9 +228,9 @@@ public class SelectionClauseWriter exte * @return value of {@link SelectionClause#isInvalid} flag, for allowing caller to short-circuit. */ @SuppressWarnings("unchecked") - final boolean write(final SelectionClause sql, final Filter<? super Feature> filter) { - visit((Filter<Feature>) filter, sql); + final boolean write(final SelectionClause sql, final Filter<? super AbstractFeature> filter) { + visit((Filter<AbstractFeature>) filter, sql); - return sql.isInvalid; + return sql.isInvalid(); } /** @@@ -240,9 -241,9 +240,9 @@@ * @param expression the expression for which to execute an action based on its type. * @return value of {@link SelectionClause#isInvalid} flag, for allowing caller to short-circuit. */ - private boolean write(final SelectionClause sql, final Expression<Feature, ?> expression) { + private boolean write(final SelectionClause sql, final Expression<AbstractFeature, ?> expression) { visit(expression, sql); - return sql.isInvalid; + return sql.isInvalid(); } /** @@@ -386,9 -387,10 +386,10 @@@ /** * Appends a function name with an arbitrary number of parameters (potentially zero). * This method stops immediately if a parameter cannot be expressed in SQL, leaving - * the trailing part of the SQL in an invalid state. + * the trailing part of the SQL in an invalid state. Callers should check if this is + * the case by invoking {@link SelectionClause#isInvalid()} after this method call. */ - private final class Function implements BiConsumer<Filter<Feature>, SelectionClause> { + private final class Function implements BiConsumer<Filter<AbstractFeature>, SelectionClause> { /** Name the function. */ final String name; @@@ -397,10 -399,27 +398,27 @@@ this.name = name; } - /** Writes the function as an SQL statement. */ + /** + * Writes the function as an SQL statement. The function is usually spatial (with geometry operands), + * but not necessarily. If the given {@code filter} contains geometry operands specified as literal, + * {@link org.apache.sis.filter.Optimization} should have already transformed the literals to the CRS + * of the geometry column when those CRS are known. Therefore, it should not be needed to perform any + * geometry transformation in this method. + */ - @Override public void accept(final Filter<Feature> filter, final SelectionClause sql) { + @Override public void accept(final Filter<AbstractFeature> filter, final SelectionClause sql) { - sql.append(name); - writeParameters(sql, filter.getExpressions(), ", ", false); + sql.appendSpatialFunction(name); - final List<Expression<Feature, ?>> expressions = filter.getExpressions(); ++ final List<Expression<AbstractFeature, ?>> expressions = filter.getExpressions(); + if (SelectionClause.REPLACE_UNSPECIFIED_CRS) { - for (Expression<Feature,?> exp : expressions) { ++ for (Expression<AbstractFeature,?> exp : expressions) { + if (exp instanceof ValueReference<?,?>) { - if (sql.acceptColumnCRS((ValueReference<Feature,?>) exp)) { ++ if (sql.acceptColumnCRS((ValueReference<AbstractFeature,?>) exp)) { + break; + } + } + } + } + writeParameters(sql, expressions, ", ", false); + sql.clearColumnCRS(); } } } diff --cc endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Table.java index e2157aaafb,808c4b80b7..7d53b4d7d6 --- 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 @@@ -36,11 -41,15 +41,13 @@@ import org.apache.sis.util.Exceptions import org.apache.sis.util.collection.WeakValueHashMap; import org.apache.sis.util.collection.TreeTable; 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.Expression; -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; /** @@@ -184,25 -193,94 +191,94 @@@ final class Table extends AbstractFeatu /** * 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. * - * @todo This constructor is not yet used because it is an unfinished work. - * We need to invent some mechanism for using a subset of the columns. - * A starting point is {@link org.apache.sis.storage.FeatureQuery#expectedType(DefaultFeatureType)}. + * @param table 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. - * @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. */ - Table(final Table parent) { - super(parent.listeners, false); - database = parent.database; - query = parent.query; - name = parent.name; - - // TODO: filter the columns. - primaryKey = parent.primaryKey; - attributes = parent.attributes; - importedKeys = parent.importedKeys; - exportedKeys = parent.exportedKeys; - featureType = parent.featureType; - hasGeometry = parent.hasGeometry; - hasRaster = parent.hasRaster; + @SuppressWarnings("LocalVariableHidesMemberVariable") + Table(final Table source, + final FeatureProjection projection, + final Set<String> reusedNames, - final Map<String, Expression<? super Feature, ?>> unhandled) ++ final Map<String, Expression<? super AbstractFeature, ?>> unhandled) + { + super(source.listeners, false); + database = source.database; + query = source.query; + name = source.name; + featureType = projection.featureType; + /* + * Temporary values the fields, before assignment to final fields. + * The final number of attributes and foreigner keys may be smaller. + */ + final var attributes = new Column [source.attributes .length]; + final var importedKeys = new Relation[source.importedKeys.length]; + final var exportedKeys = new Relation[source.exportedKeys.length]; + int attributesCount = 0; + int importedKeysCount = 0; + int exportedKeysCount = 0; + boolean hasGeometry = false; + /* + * Take all columns that we can put in the `WHERE` clause of the SQL statement. + * The columns that we cannot take will be declared in the `unhandled` bit set. + */ + final List<String> storedProperties = projection.getStoredPropertyNames(); + final int count = storedProperties.size(); + for (int i=0; i<count; i++) { + final String xpath = projection.getSourcePropertyName(i, false); + if (xpath == null) { + unhandled.put(storedProperties.get(i), projection.getExpression(i)); + continue; + } + final Column column = source.getColumn(xpath); + if (column != null) { + hasGeometry |= column.getGeometryType().isPresent(); + final String name = storedProperties.get(i); + final Column renamed = column.rename(name); + attributes[attributesCount++] = renamed; + if (renamed == column) { + reusedNames.add(name); + } + continue; + } + Relation relation = source.getRelation(xpath, false); + if (relation != null) { + importedKeys[importedKeysCount++] = relation; + continue; + } + relation = source.getRelation(xpath, false); + if (relation != null) { + exportedKeys[exportedKeysCount++] = relation; + continue; + } - throw new InvalidFilterValueException(Errors.forLocale(listeners.getLocale()) ++ throw new IllegalArgumentException(Errors.forLocale(listeners.getLocale()) + .getString(Errors.Keys.PropertyNotFound_2, source.featureType.getName(), xpath)); + } + /* + * Save the columns and foreigner keys that this table handles. + * Take the primary key only if we have all the needed columns + * and those columns have the same names as in the source table. + */ + this.hasGeometry = hasGeometry; + this.hasRaster = source.hasRaster; // Not yet accurately determined. + this.attributes = ArraysExt.resize(attributes, attributesCount); + this.importedKeys = ArraysExt.resize(importedKeys, importedKeysCount); + this.exportedKeys = ArraysExt.resize(exportedKeys, exportedKeysCount); + if (source.primaryKey != null) { + for (String column : source.primaryKey.getColumns()) { + final int i = storedProperties.indexOf(column); + if (i < 0 || !column.equals(projection.getSourcePropertyName(i, true))) { + primaryKey = null; + return; + } + } + } + this.primaryKey = source.primaryKey; } /** @@@ -227,7 -305,7 +303,7 @@@ * have been set to association names. If `ClassCastException` occurs here, it is a bug * in our object constructions. */ - final var association = (DefaultAssociationRole) featureType.getProperty(relation.propertyName); - final var association = (FeatureAssociationRole) featureType.getProperty(relation.getPropertyName()); ++ final var association = (DefaultAssociationRole) featureType.getProperty(relation.getPropertyName()); final Table table = tables.get(association.getValueType().getName()); if (table == null) { throw new InternalDataStoreException(association.toString()); diff --cc endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java index 3f8bb5a415,989576bb5b..fe55cf6f5c --- 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 @@@ -172,10 -174,10 +172,10 @@@ public final class SQLStoreTest extend * Creates a {@link SQLStore} instance with the specified table as a resource, then tests some queries. */ private void testTableQuery(final StorageConnector connector, final ResourceDefinition table) throws Exception { - try (SimpleFeatureStore store = new SimpleFeatureStore(new SQLStoreProvider(), connector, table)) { + try (var store = new SimpleFeatureStore(new SQLStoreProvider(), connector, table)) { verifyFeatureTypes(store); - final Map<String,Integer> countryCount = new HashMap<>(); + final var countryCount = new HashMap<String,Integer>(); - try (Stream<Feature> features = store.findResource("Cities").features(false)) { + try (Stream<AbstractFeature> features = store.findResource("Cities").features(false)) { features.forEach((f) -> verifyContent(f, countryCount)); } assertEquals(Integer.valueOf(2), countryCount.remove("CAN")); @@@ -285,7 -287,7 +285,7 @@@ */ assertEquals(c.countryName, getIndirectPropertyValue(feature, "country", "native_name")); if (isCanada) { - final AbstractFeature f = (AbstractFeature) feature.getPropertyValue("country"); - final var f = (Feature) feature.getPropertyValue("country"); ++ final var f = (AbstractFeature) feature.getPropertyValue("country"); if (canada == null) { canada = f; } else { @@@ -447,9 -451,9 +447,9 @@@ * @param cities a feature set containing all cities defined for the test class. */ private void verifyStreamOperations(final FeatureSet cities) throws DataStoreException { - try (Stream<Feature> features = cities.features(false)) { + try (Stream<AbstractFeature> features = cities.features(false)) { - final AtomicInteger peekCount = new AtomicInteger(); - final AtomicInteger mapCount = new AtomicInteger(); + final var peekCount = new AtomicInteger(); + final var mapCount = new AtomicInteger(); final long actualPopulations = features.peek(f -> peekCount.incrementAndGet()) .peek(f -> peekCount.incrementAndGet()) .map (f -> {mapCount.incrementAndGet(); return f;}) @@@ -483,8 -487,8 +483,8 @@@ "SELECT * FROM " + SCHEMA + ".\"Cities\" WHERE \"population\" >= 1000000"))) { final FeatureSet cities = store.findResource("LargeCities"); - final Map<String,Integer> countryCount = new HashMap<>(); + final var countryCount = new HashMap<String,Integer>(); - try (Stream<Feature> features = cities.features(false)) { + try (Stream<AbstractFeature> features = cities.features(false)) { features.forEach((f) -> verifyContent(f, countryCount)); } assertEquals(Integer.valueOf(1), countryCount.remove("CAN")); @@@ -506,7 -510,8 +506,7 @@@ /* * Add a filter for parks in France. */ - final FeatureQuery query = new FeatureQuery(); + final var query = new FeatureQuery(); - query.setSortBy(FF.sort(FF.property("native_name"), SortOrder.DESCENDING)); query.setSelection(FF.equal(FF.property("country"), FF.literal("FRA"))); query.setProjection(FF.property("native_name")); final FeatureSet frenchParks = parks.subset(query); @@@ -540,8 -545,8 +540,8 @@@ "OFFSET 2 ROWS FETCH NEXT 3 ROWS ONLY"))) { final FeatureSet parks = store.findResource("MyQuery"); - final FeatureType type = parks.getType(); - final var property = (AttributeType<?>) TestUtilities.getSingleton(type.getProperties(true)); + final DefaultFeatureType type = parks.getType(); - final DefaultAttributeType<?> property = (DefaultAttributeType<?>) TestUtilities.getSingleton(type.getProperties(true)); ++ final var property = (DefaultAttributeType<?>) TestUtilities.getSingleton(type.getProperties(true)); assertEquals("title", property.getName().toString(), "Property name should be label defined in query"); assertEquals(String.class, property.getValueClass(), "Attribute should be a string"); assertEquals(0, property.getMinimumOccurs(), "Column should be nullable."); diff --cc endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java index a0c47e6831,f9ed98cc6b..aed3d72300 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/postgis/PostgresTest.java @@@ -58,8 -67,8 +67,9 @@@ import org.apache.sis.test.TestUtilitie import org.apache.sis.metadata.sql.TestDatabase; import org.apache.sis.referencing.crs.HardCodedCRS; -// Specific to the geoapi-3.1 and geoapi-4.0 branches: -import org.opengis.feature.Feature; +// Specific to the main branch: +import org.apache.sis.feature.AbstractFeature; ++import org.apache.sis.metadata.iso.identification.AbstractIdentification; /** @@@ -99,6 -108,19 +109,20 @@@ public final class PostgresTest extend assertNull ( version.getRevision()); } + /** + * Performs some verification of store metadata. + * + * @param metadata the metadata to verify. + */ + private static void validate(final Metadata metadata) { + final Identification identification = TestUtilities.getSingleton(metadata.getIdentificationInfo()); - assertTrue(identification.getSpatialRepresentationTypes().containsAll( ++ var defId = assertInstanceOf(AbstractIdentification.class, identification); ++ assertTrue(defId.getSpatialRepresentationTypes().containsAll( + Arrays.asList(SpatialRepresentationType.TEXT_TABLE, + SpatialRepresentationType.VECTOR, + SpatialRepresentationType.GRID))); + } + /** * Tests reading and writing features and rasters. * @@@ -187,6 -207,62 +209,62 @@@ } } + /** + * Tests iterating over all features without filters. + * Opportunistically verifies also the bounding boxes of all features. + * + * @param resource the set of all features. + * @throws DataStoreException if an error occurred while fetching the envelope of feature instances. + */ + private static void testAllFeatures(final FeatureSet resource) throws DataStoreException { + final Envelope envelope = resource.getEnvelope().get(); + assertEquals(-72, envelope.getMinimum(0), 1); + assertEquals( 43, envelope.getMaximum(1), 1); - try (Stream<Feature> features = resource.features(false)) { ++ try (Stream<AbstractFeature> features = resource.features(false)) { + features.forEach(PostgresTest::validate); + } - try (Stream<Feature> features = resource.features(false)) { ++ try (Stream<AbstractFeature> features = resource.features(false)) { + assertEquals(8, features.count()); + } + } + + /** + * Tests iterating over features with a filter applied, with our without coordinate operation. + * The filter is {@code ST_Intersects(geometry, literal)} where the literal is a polygon. + * The coordinate operation is simply an axis swapping. + * + * @param resource the set of all features. + * @param transform whether to apply a coordinate operation. + * @throws Exception if an error occurred while fetching the envelope of feature instances. + */ + private static void testFilteredFeatures(final FeatureSet resource, final boolean transform) throws Exception { + final Coordinate[] coordinates = { + new Coordinate(-72, 42), + new Coordinate(-71, 42), + new Coordinate(-71, 43), + new Coordinate(-72, 43), + new Coordinate(-72, 42) + }; + if (transform) { + for (Coordinate c : coordinates) { + double swp = c.x; + c.x = c.y; + c.y = swp; + } + } + final Geometry geom = new GeometryFactory().createPolygon(coordinates); + if (transform) { + JTS.setCoordinateReferenceSystem(geom, CommonCRS.WGS84.geographic()); + } else { + geom.setSRID(4326); + } + final var factory = DefaultFilterFactory.forFeatures(); + final var filter = factory.intersects(factory.property("geom4326"), factory.literal(geom)); - try (Stream<Feature> features = resource.features(false).filter(filter)) { ++ try (Stream<AbstractFeature> features = resource.features(false).filter(filter)) { + assertEquals(4, features.count()); + } + } + /** * Invoked for each feature instances for performing some checks on the feature. * This method performs only a superficial verification of geometries. diff --cc endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java index 70896c5751,7a961f9af1..5f2d84d995 --- 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,23 -36,22 +36,19 @@@ import org.apache.sis.filter.privy.Sort import org.apache.sis.storage.internal.Resources; import org.apache.sis.pending.jdk.JDK19; import org.apache.sis.util.ArgumentChecks; - import org.apache.sis.util.ArraysExt; import org.apache.sis.util.CharSequences; + import org.apache.sis.util.Emptiable; import org.apache.sis.util.iso.Names; - 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.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.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.DefaultFeatureType; - import org.apache.sis.feature.DefaultAttributeType; +import org.apache.sis.feature.AbstractAttribute; +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; /** @@@ -142,10 -137,10 +134,10 @@@ public class FeatureQuery extends Quer * @see #setSortBy(SortBy) */ @SuppressWarnings("serial") // Most SIS implementations are serializable. - private SortBy<Feature> sortBy; + private SortBy<AbstractFeature> sortBy; /** - * Hint used by resources to optimize returned features. + * Hint used by resources to optimize returned features, or {@code null} for full resolution. * Different stores make use of vector tiles of different scales. * A {@code null} value means to query data at their full resolution. * @@@ -191,15 -220,17 +217,17 @@@ * delegates to {@link #setProjection(NamedExpression...)}. * * @param properties properties to retrieve, or {@code null} to retrieve all properties. - * @throws IllegalArgumentException if a property is duplicated. + * @throws IllegalArgumentException if the given array is empty of if a property is duplicated. */ @SafeVarargs + @SuppressWarnings("varargs") - public final void setProjection(final Expression<? super Feature, ?>... properties) { + public final void setProjection(final Expression<? super AbstractFeature, ?>... properties) { NamedExpression[] wrappers = null; if (properties != null) { + ArgumentChecks.ensureNonEmpty("properties", properties); wrappers = new NamedExpression[properties.length]; for (int i=0; i<wrappers.length; i++) { - final Expression<? super Feature, ?> e = properties[i]; + final Expression<? super AbstractFeature, ?> e = properties[i]; ArgumentChecks.ensureNonNullElement("properties", i, e); wrappers[i] = new NamedExpression(e); } diff --cc endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureSubset.java index a01e984e1a,32159973bd..88f140538c --- 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 @@@ -24,12 -24,11 +24,11 @@@ import org.apache.sis.storage.base.Meta import org.apache.sis.storage.base.StoreUtilities; import org.apache.sis.storage.internal.Resources; -// 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.Filter; -import org.opengis.filter.SortBy; +// Specific to the main branch: +import org.apache.sis.feature.AbstractFeature; +import org.apache.sis.feature.DefaultFeatureType; - import org.apache.sis.filter.Expression; +import org.apache.sis.filter.Filter; +import org.apache.sis.pending.geoapi.filter.SortBy; /** @@@ -86,11 -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 { - resultType = query.expectedType(type); + projection = FeatureProjection.create(type, query.getProjection()); + resultType = (projection != null) ? projection.featureType : type; } catch (IllegalArgumentException e) { throw new DataStoreContentException(Resources.forLocale(listeners.getLocale()) .getString(Resources.Keys.CanNotDeriveTypeFromFeature_1, type.getName()), e); diff --cc endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/FeatureProjection.java index 0000000000,5a25571e28..f31860f74d mode 000000,100644..100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/FeatureProjection.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/FeatureProjection.java @@@ -1,0 -1,383 +1,381 @@@ + /* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.apache.sis.storage.base; + + import java.util.List; + import java.util.Map; + import java.util.Set; + import java.util.ConcurrentModificationException; + import java.util.function.UnaryOperator; + import org.opengis.util.GenericName; -import org.apache.sis.feature.AbstractOperation; + import org.apache.sis.feature.FeatureOperations; + import org.apache.sis.feature.builder.FeatureTypeBuilder; + import org.apache.sis.feature.builder.PropertyTypeBuilder; + import org.apache.sis.feature.builder.AttributeTypeBuilder; + import org.apache.sis.feature.privy.FeatureExpression; + import org.apache.sis.filter.privy.XPath; + import org.apache.sis.storage.FeatureQuery; + import org.apache.sis.storage.internal.Resources; + import org.apache.sis.pending.jdk.JDK19; + import org.apache.sis.util.ArraysExt; + import org.apache.sis.util.iso.Names; + import org.apache.sis.util.logging.Logging; + import org.apache.sis.util.privy.UnmodifiableArrayList; + import org.apache.sis.util.resources.Vocabulary; + -// Specific to the geoapi-3.1 and geoapi-4.0 branches: -import org.opengis.feature.Feature; -import org.opengis.feature.FeatureType; -import org.opengis.feature.PropertyNotFoundException; -import org.opengis.filter.InvalidFilterValueException; -import org.opengis.filter.Expression; -import org.opengis.filter.ValueReference; ++// Specific to the main branch: ++import org.apache.sis.feature.AbstractFeature; ++import org.apache.sis.feature.DefaultFeatureType; ++import org.apache.sis.feature.AbstractOperation; ++import org.apache.sis.filter.Expression; ++import org.apache.sis.pending.geoapi.filter.ValueReference; + + + /** + * A function applying projections (with "projected" in the <abbr>SQL</abbr> database sense) of features. + * Given full feature instances in input, this method returns feature instances containing only a subset + * of the properties. The property values may also be different if they are computed by the expressions. + * + * @author Guilhem Legal (Geomatys) + * @author Martin Desruisseaux (Geomatys) + */ -public class FeatureProjection implements UnaryOperator<Feature> { ++public class FeatureProjection implements UnaryOperator<AbstractFeature> { + /** + * The type of features created by this mapper. + */ - public final FeatureType featureType; ++ public final DefaultFeatureType featureType; + + /** + * Names of the properties to be stored in the target features. This is inferred from the {@code properties} + * given at construction time, retaining only the elements of type {@link FeatureQuery.ProjectionType#STORED}. + * Properties that are computed on-the-fly from other properties are not included in this list. + * This array is arbitrarily set to {@code null} if this projection is an identity operation. + * + * <p>When a new {@code Feature} instance needs to be created with a subset of the properties + * of the original feature instance, usually only the stored properties need to be copied. + * Other properties computed on-the-fly from stored properties may not need copy.</p> + * + * @see #isIdentity() + */ + private final String[] storedProperties; + + /** + * Expressions to apply on the source feature for fetching the property values of the projected feature. + * Shall have the same length as {@link #storedProperties} and the properties shall be in the same order. + * This array is arbitrarily set to {@code null} if this projection is an identity operation. + * + * @see #isIdentity() + */ - private final Expression<? super Feature, ?>[] expressions; ++ private final Expression<? super AbstractFeature, ?>[] expressions; + + /** + * Infers the type of values evaluated by a query when executed on features of the given type. + * If some expressions have no name, default names are computed as below: + * + * <ul> + * <li>If the expression is an instance of {@link ValueReference}, the name of the + * property referenced by the {@linkplain ValueReference#getXPath() XPath}.</li> + * <li>Otherwise the localized string "Unnamed #1" with increasing numbers.</li> + * </ul> + * + * <h4>Identity operation</h4> + * If the result is a feature type with all the properties of the source feature, + * with the same property names in the same order, and if the expressions are only + * fetching the values (no computation), then this method returns {@code null} for + * meaning that this projection does nothing. + * + * @param sourceType the type of features to be converted to projected features. + * @param projection descriptions of the properties to keep in the projected features, or {@code null} if none. + * @return a function for projecting the feature instances, or {@code null} if none. + * @throws IllegalArgumentException if this method can operate only on some feature types + * and the given type is not one of them. - * @throws InvalidFilterValueException if this method cannot determine the result type of an expression. ++ * @throws IllegalArgumentException if this method cannot determine the result type of an expression. + * It may be because that expression is backed by an unsupported implementation. + */ - public static FeatureProjection create(final FeatureType sourceType, final FeatureQuery.NamedExpression[] projection) { ++ public static FeatureProjection create(final DefaultFeatureType sourceType, final FeatureQuery.NamedExpression[] projection) { + if (projection != null) { + var fp = new FeatureProjection(sourceType, projection); + if (!fp.isIdentity()) { + return fp; + } + } + return null; + } + + /** + * Creates a new feature projection. If some expressions have no name, + * default names are computed as described in {@link #create create(…)}. + * + * <p>Callers shall invoke {@link #isIdentity()} after construction. + * This instance shall not be used if it is the identity operation.</p> + * + * @param sourceType the type of features to be converted to projected features. + * @param projection descriptions of the properties to keep in the projected features, or {@code null} if none. + * @throws IllegalArgumentException if this method can operate only on some feature types + * and the given type is not one of them. - * @throws InvalidFilterValueException if this method cannot determine the result type of an expression. ++ * @throws IllegalArgumentException if this method cannot determine the result type of an expression. + * It may be because that expression is backed by an unsupported implementation. + */ - protected FeatureProjection(final FeatureType sourceType, final FeatureQuery.NamedExpression[] projection) { ++ protected FeatureProjection(final DefaultFeatureType sourceType, final FeatureQuery.NamedExpression[] projection) { + int storedCount = 0; + int unnamedNumber = 0; // Sequential number for unnamed expressions. + Set<String> names = null; // Names already used, for avoiding collisions. + + // For detecting if the projection would be an identity operation. + var propertiesOfIdentity = sourceType.getProperties(true).iterator(); + + @SuppressWarnings({"LocalVariableHidesMemberVariable", "unchecked", "rawtypes"}) - final Expression<? super Feature,?>[] expressions = new Expression[projection.length]; ++ final Expression<? super AbstractFeature,?>[] expressions = new Expression[projection.length]; + + @SuppressWarnings("LocalVariableHidesMemberVariable") + final String[] storedProperties = new String[projection.length]; + + final FeatureTypeBuilder ftb = new FeatureTypeBuilder().setName(sourceType.getName()); + for (int column = 0; column < projection.length; column++) { + final FeatureQuery.NamedExpression item = projection[column]; + /* + * For each property, get the expected type (mandatory) and its name (optional). + * A default name will be computed if no alias was explicitly given by the user. + */ + final var expression = item.expression; + final var fex = FeatureExpression.castOrCopy(expression); + final PropertyTypeBuilder resultType; + if (fex == null || (resultType = fex.expectedType(sourceType, ftb)) == null) { - throw new InvalidFilterValueException(Resources.format(Resources.Keys.InvalidExpression_2, ++ throw new IllegalArgumentException(Resources.format(Resources.Keys.InvalidExpression_2, + expression.getFunctionName().toInternationalString(), column)); + } + GenericName name = item.alias; + if (name == null) { + /* + * Build a list of aliases declared by the user, for making sure that we do not collide with them. + * No check for `GenericName` collision here because it was already verified by `setProjection(…)`. + * We may have collision of their `String` representations however, which is okay. + */ + if (names == null) { + names = JDK19.newHashSet(projection.length); + for (final FeatureQuery.NamedExpression p : projection) { + if (p.alias != null) { + names.add(p.alias.toString()); + } + } + } + /* + * If the expression is a `ValueReference`, the `PropertyType` instance can be taken directly + * from the source feature (the Apache SIS implementation does just that). If the name is set, + * then we assume that it is correct. Otherwise we take the tip of the XPath. + */ + String tip = reference(expression, true); + if (tip != null) { + /* + * Take the existing `GenericName` instance from the property. It should be equivalent to + * creating a name from the tip, except that it may be a `ScopedName` instead of `LocalName`. + * We do not take `resultType.getName()` because the latter is different if the property + * is itself a link to another property (in which case `resultType` is the final target). + */ + name = sourceType.getProperty(tip).getName(); + if (name == null || !names.add(name.toString())) { + name = null; + if (tip.isBlank() || names.contains(tip)) { + tip = null; + } + } + } + /* + * If we still have no name at this point, create a name like "Unnamed #1". + * Note that despite the use of `Vocabulary` resources, the name will be unlocalized + * (for easier programmatic use) because `GenericName` implementation is designed for + * providing localized names only if explicitly requested. + */ + if (name == null) { + CharSequence text = tip; + if (text == null) do { + text = Vocabulary.formatInternational(Vocabulary.Keys.Unnamed_1, ++unnamedNumber); + } while (!names.add(text.toString())); + name = Names.createLocalName(null, null, text); + } + } + /* + * If the attribute that we just added should be virtual, replace the attribute by an operation. + * When `FeatureProjection` contains at least one such virtual attributes, the result cannot be + * an identity operation. + */ + if (item.type != FeatureQuery.ProjectionType.STORED) { + propertiesOfIdentity = null; // No longer an identity operation. + if (resultType instanceof AttributeTypeBuilder<?>) { + final var ab = (AttributeTypeBuilder<?>) resultType; + final var storedType = ab.build(); + if (ftb.properties().remove(resultType)) { + final var identification = Map.of(AbstractOperation.NAME_KEY, name); + ftb.addProperty(FeatureOperations.expression(identification, expression, storedType)); + } + } + continue; + } + /* + * This is the usual case where the property value is copied in the "projected" feature. + * If the target property name is equal to the source property name, in the same order, + * and the expression is just fetching the value from a property of the same name, then + * we consider that the projection is an identity operation. + */ + resultType.setName(name); + storedProperties[storedCount] = name.toString(); + expressions[storedCount++] = expression; + isIdentity: if (propertiesOfIdentity != null) { + if (propertiesOfIdentity.hasNext()) { + final var property = propertiesOfIdentity.next(); + if (name.equals(property.getName())) { + final String tip = name.tip().toString(); + if (tip.equals(reference(expression, true))) try { + if (property.equals(sourceType.getProperty(tip))) { + break isIdentity; // Continue to consider that we may have an identity projection. + } - } catch (PropertyNotFoundException e) { ++ } catch (IllegalArgumentException e) { + // It may be because the property name is ambiguous. + Logging.ignorableException(StoreUtilities.LOGGER, FeatureProjection.class, "create", e); + } + } + } + propertiesOfIdentity = null; + } + } + /* + * End of the construction of properties from the given expressions. If all properties + * are just copying values of properties of the same name from the source, returns `null`. + */ + if (propertiesOfIdentity == null || propertiesOfIdentity.hasNext()) { + this.featureType = ftb.build(); + this.storedProperties = ArraysExt.resize(storedProperties, storedCount); + this.expressions = ArraysExt.resize(expressions, storedCount); + } else { + this.featureType = sourceType; + this.storedProperties = null; + this.expressions = null; // Means "identity operation". + } + } + + /** + * Creates a new projection with the given properties. + * + * @param type the type of feature instances created by this projection. + * @param projection all properties by name, associated to expressions for getting values from source features. + */ + @SuppressWarnings({"rawtypes", "unchecked"}) - public FeatureProjection(final FeatureType type, final Map<String, Expression<? super Feature, ?>> projection) { ++ public FeatureProjection(final DefaultFeatureType type, final Map<String, Expression<? super AbstractFeature, ?>> projection) { + featureType = type; + expressions = new Expression[projection.size()]; + storedProperties = new String[expressions.length]; + int i = 0; + for (var entry : projection.entrySet()) { + storedProperties[i] = entry.getKey(); + expressions[i++] = entry.getValue(); + } + if (i != expressions.length) { + // Should never happen, unless the `Map` has been modified concurrently. + throw new ConcurrentModificationException(); + } + } + + /** + * If the given expression is a value reference, returns the tip of the referenced property. + * Otherwise, returns {@code null}. + * + * @param expression the expression from which to get the referenced property name. + * @param decode whether to decode the XPath syntax (e.g. {@code {@code "Q{namespace}"}} syntax). + * @return the referenced property name, or {@code null} if none. + */ - private static String reference(final Expression<? super Feature, ?> expression, final boolean decode) { ++ private static String reference(final Expression<? super AbstractFeature, ?> expression, final boolean decode) { + if (expression instanceof ValueReference<?,?>) { + String xpath = ((ValueReference<?,?>) expression).getXPath(); + return decode ? XPath.toPropertyName(xpath) : xpath; + } + return null; + } + + /** + * Returns whether this projection performs no operation. + * If this method returns {@code true}, then this instance shall <strong>not</strong> be used. + * + * @return whether this projection performs no operation. + */ + public final boolean isIdentity() { + return expressions == null; + } + + /** + * Returns the names of all stored properties. This list may be shorter than the list of properties + * of the {@linkplain #featureType feature type} if some feature properties are computed on-the-fly. + * + * @return the name of all stored properties. + */ + public final List<String> getStoredPropertyNames() { + return UnmodifiableArrayList.wrap(storedProperties); + } + + /** + * Returns the name of a property in the source features. + * The given index corresponds to an index in the list returned by {@link #getStoredPropertyNames()}. + * The return value is often the same name as {@code getStoredPropertyNames().get(index)}. + * + * @param index index of the stored property for which to get the name in the source feature. + * @param decode whether to decode the XPath syntax (e.g. {@code {@code "Q{namespace}"}} syntax). + * @return name in the source features, or {@code null} if the property is not a {@link ValueReference}. + * @throws NullPointerException if {@link #isIdentity()} is {@code true}. + */ + public final String getSourcePropertyName(final int index, final boolean decode) { + return reference(expressions[index], decode); + } + + /** + * Returns the expression at the given index. + * + * @param index index of the stored property for which to get the expression. + * @return expression at the given index. + * @throws NullPointerException if {@link #isIdentity()} is {@code true}. + */ - public final Expression<? super Feature, ?> getExpression(final int index) { ++ public final Expression<? super AbstractFeature, ?> getExpression(final int index) { + return expressions[index]; + } + + /** + * Derives a new projected feature instance from the given source. + * + * @param source the source feature instance. + * @return the "projected" (<abbr>SQL</abbr> database sense) feature instance. + * @throws NullPointerException if {@link #isIdentity()} is {@code true}. + */ + @Override - public final Feature apply(final Feature source) { ++ public final AbstractFeature apply(final AbstractFeature source) { + final var feature = featureType.newInstance(); + for (int i=0; i < expressions.length; i++) { + feature.setPropertyValue(storedProperties[i], expressions[i].apply(source)); + } + return feature; + } + + /** + * Applies the expressions with the given feature as both the source and the target. + * IT can be useful when all expressions are computing values derived from other attributes. + * + * @param feature the feature on which to apply the expressions. + * @throws NullPointerException if {@link #isIdentity()} is {@code true}. + */ - public final void applySelf(final Feature feature) { ++ public final void applySelf(final AbstractFeature feature) { + for (int i=0; i < expressions.length; i++) { + feature.setPropertyValue(storedProperties[i], expressions[i].apply(feature)); + } + } + } diff --cc endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java index 9599d7d43f,2d26e160c4..b62caeb9ec --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/base/MetadataBuilder.java @@@ -106,9 -105,8 +105,8 @@@ import org.apache.sis.coverage.grid.Gri import org.apache.sis.pending.jdk.JDK21; import org.apache.sis.measure.Units; -// Specific to the geoapi-3.1 and geoapi-4.0 branches: -import org.opengis.feature.FeatureType; +// Specific to the main branch: - import org.opengis.referencing.ReferenceIdentifier; +import org.apache.sis.feature.DefaultFeatureType; /** diff --cc endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java index d97c6f8206,49a8f0eb12..2e672361f0 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/WorldFileStore.java @@@ -533,9 -514,9 +514,9 @@@ loop: for (int convention=0;; convent break; } } - builder.addFormatName(format); // Does nothing if `format` is null. + builder.addFormatName(format); // Does nothing if `format` is null. builder.addFormatReaderSIS(WorldFileStoreProvider.NAME); - builder.addResourceScope(ScopeCode.COVERAGE, null); + builder.addResourceScope(ScopeCode.valueOf("COVERAGE"), null); builder.addSpatialRepresentation(null, getGridGeometry(MAIN_IMAGE), true); if (gridGeometry.isDefined(GridGeometry.ENVELOPE)) { builder.addExtent(gridGeometry.getEnvelope(), listeners); diff --cc endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java index 36958d18fd,6bd071f8f3..dc9a8758ee --- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java +++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java @@@ -33,15 -33,22 +33,16 @@@ import org.junit.jupiter.api.Test import static org.junit.jupiter.api.Assertions.*; import org.apache.sis.test.TestUtilities; import org.apache.sis.test.TestCase; + import static org.apache.sis.test.Assertions.assertSetEquals; import static org.apache.sis.test.Assertions.assertMessageContains; -// 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.IdentifiedType; -import org.opengis.feature.Operation; -import org.opengis.filter.Expression; -import org.opengis.filter.Filter; -import org.opengis.filter.FilterFactory; -import org.opengis.filter.MatchAction; -import org.opengis.filter.SortOrder; -import org.opengis.filter.SortProperty; +// Specific to the main branch: +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.feature.AbstractOperation; +import org.apache.sis.filter.Expression; /** @@@ -196,9 -230,10 +209,10 @@@ public final class FeatureQueryTest ext @Test public void testSelection() throws DataStoreException { createFeaturesWithAssociation(); - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); + final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); query.setSelection(ff.equal(ff.property("value1", Integer.class), - ff.literal(2), true, MatchAction.ALL)); + ff.literal(2))); + assertXPathsEqual("value1"); verifyQueryResult(1, 2); } @@@ -211,8 -246,9 +225,9 @@@ @Test public void testSelectionThroughAssociation() throws DataStoreException { createFeaturesWithAssociation(); - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); + final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); query.setSelection(ff.equal(ff.property("dependency/value3"), ff.literal(18))); + assertXPathsEqual("dependency/value3"); verifyQueryResult(3); } @@@ -228,21 -264,22 +243,22 @@@ query.setProjection(new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) null), new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), "renamed1"), new FeatureQuery.NamedExpression(ff.literal("a literal"), "computed")); + assertXPathsEqual("value1"); // Check result type. - final Feature instance = executeAndGetFirst(); - final FeatureType resultType = instance.getType(); + final AbstractFeature instance = executeAndGetFirst(); + final DefaultFeatureType resultType = instance.getType(); assertEquals("Test", resultType.getName().toString()); assertEquals(3, resultType.getProperties(true).size()); - final PropertyType pt1 = resultType.getProperty("value1"); - final PropertyType pt2 = resultType.getProperty("renamed1"); - final PropertyType pt3 = resultType.getProperty("computed"); - assertTrue(pt1 instanceof AttributeType); - assertTrue(pt2 instanceof AttributeType); - assertTrue(pt3 instanceof AttributeType); - assertEquals(Integer.class, ((AttributeType) pt1).getValueClass()); - assertEquals(Integer.class, ((AttributeType) pt2).getValueClass()); - assertEquals(String.class, ((AttributeType) pt3).getValueClass()); + final AbstractIdentifiedType pt1 = resultType.getProperty("value1"); + final AbstractIdentifiedType pt2 = resultType.getProperty("renamed1"); + final AbstractIdentifiedType pt3 = resultType.getProperty("computed"); + assertTrue(pt1 instanceof DefaultAttributeType); + assertTrue(pt2 instanceof DefaultAttributeType); + assertTrue(pt3 instanceof DefaultAttributeType); + assertEquals(Integer.class, ((DefaultAttributeType) pt1).getValueClass()); + assertEquals(Integer.class, ((DefaultAttributeType) pt2).getValueClass()); + assertEquals(String.class, ((DefaultAttributeType) pt3).getValueClass()); // Check feature instance. assertEquals(3, instance.getPropertyValue("value1")); @@@ -259,8 -296,9 +275,9 @@@ public void testProjectionByNames() throws DataStoreException { createFeaturesWithAssociation(); query.setProjection("value2"); + assertXPathsEqual("value2"); - final Feature instance = executeAndGetFirst(); - final PropertyType p = TestUtilities.getSingleton(instance.getType().getProperties(true)); + final AbstractFeature instance = executeAndGetFirst(); + final AbstractIdentifiedType p = TestUtilities.getSingleton(instance.getType().getProperties(true)); assertEquals("value2", p.getName().toString()); } @@@ -278,9 -316,10 +295,10 @@@ query.setProjection( ff.add(ff.property("value1", Number.class), ff.literal(1)), ff.add(ff.property("value2", Number.class), ff.literal(1))); + assertXPathsEqual("value1", "value2"); final FeatureSet subset = featureSet.subset(query); - final FeatureType type = subset.getType(); - final Iterator<? extends PropertyType> properties = type.getProperties(true).iterator(); + final DefaultFeatureType type = subset.getType(); + final Iterator<? extends AbstractIdentifiedType> properties = type.getProperties(true).iterator(); assertEquals("Unnamed #1", properties.next().getName().toString()); assertEquals("Unnamed #2", properties.next().getName().toString()); assertFalse(properties.hasNext()); @@@ -299,21 -338,22 +317,22 @@@ @Test public void testProjectionOfAbstractType() throws DataStoreException { createFeaturesWithAssociation(); - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); + final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); query.setProjection(new FeatureQuery.NamedExpression(ff.property("value1"), (String) null), new FeatureQuery.NamedExpression(ff.property("/*/unknown"), "unexpected")); + assertXPathsEqual("value1", "/*/unknown"); // Check result type. - final Feature instance = executeAndGetFirst(); - final FeatureType resultType = instance.getType(); + final AbstractFeature instance = executeAndGetFirst(); + final DefaultFeatureType resultType = instance.getType(); assertEquals("Test", resultType.getName().toString()); assertEquals(2, resultType.getProperties(true).size()); - final PropertyType pt1 = resultType.getProperty("value1"); - final PropertyType pt2 = resultType.getProperty("unexpected"); - assertTrue(pt1 instanceof AttributeType<?>); - assertTrue(pt2 instanceof AttributeType<?>); - assertEquals(Integer.class, ((AttributeType<?>) pt1).getValueClass()); - assertEquals(Object.class, ((AttributeType<?>) pt2).getValueClass()); + final AbstractIdentifiedType pt1 = resultType.getProperty("value1"); + final AbstractIdentifiedType pt2 = resultType.getProperty("unexpected"); + assertTrue(pt1 instanceof DefaultAttributeType<?>); + assertTrue(pt2 instanceof DefaultAttributeType<?>); + assertEquals(Integer.class, ((DefaultAttributeType<?>) pt1).getValueClass()); + assertEquals(Object.class, ((DefaultAttributeType<?>) pt2).getValueClass()); // Check feature property values. assertEquals(3, instance.getPropertyValue("value1")); @@@ -329,11 -369,12 +348,12 @@@ @Test public void testProjectionThroughAssociation() throws DataStoreException { createFeaturesWithAssociation(); - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); + final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); query.setProjection(new FeatureQuery.NamedExpression(ff.property("value1"), (String) null), new FeatureQuery.NamedExpression(ff.property("dependency/value3"), "value3")); + assertXPathsEqual("value1", "dependency/value3"); query.setOffset(2); - final Feature instance = executeAndGetFirst(); + final AbstractFeature instance = executeAndGetFirst(); assertEquals( 2, instance.getPropertyValue("value1")); assertEquals(25, instance.getPropertyValue("value3")); } @@@ -348,7 -389,8 +368,8 @@@ public void testProjectionOfLink() throws DataStoreException { createFeatureWithIdentifier(); query.setProjection(AttributeConvention.IDENTIFIER); + assertXPathsEqual(AttributeConvention.IDENTIFIER); - final Feature instance = executeAndGetFirst(); + final AbstractFeature instance = executeAndGetFirst(); assertEquals("id-0", instance.getPropertyValue(AttributeConvention.IDENTIFIER)); } @@@ -372,25 -414,26 +393,26 @@@ new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) null), virtualProjection(ff.property("value1", Integer.class), "renamed1"), virtualProjection(ff.literal("a literal"), "computed")); + assertXPathsEqual("value1"); // Check result type. - final Feature instance = executeAndGetFirst(); - final FeatureType resultType = instance.getType(); + final AbstractFeature instance = executeAndGetFirst(); + final DefaultFeatureType resultType = instance.getType(); assertEquals("Test", resultType.getName().toString()); assertEquals(3, resultType.getProperties(true).size()); - final PropertyType pt1 = resultType.getProperty("value1"); - final PropertyType pt2 = resultType.getProperty("renamed1"); - final PropertyType pt3 = resultType.getProperty("computed"); - assertTrue(pt1 instanceof AttributeType<?>); - assertTrue(pt2 instanceof Operation); - assertTrue(pt3 instanceof Operation); - final IdentifiedType result2 = ((Operation) pt2).getResult(); - final IdentifiedType result3 = ((Operation) pt3).getResult(); - assertEquals(Integer.class, ((AttributeType<?>) pt1).getValueClass()); - assertTrue(result2 instanceof AttributeType<?>); - assertTrue(result3 instanceof AttributeType<?>); - assertEquals(Integer.class, ((AttributeType<?>) result2).getValueClass()); - assertEquals(String.class, ((AttributeType<?>) result3).getValueClass()); + final AbstractIdentifiedType pt1 = resultType.getProperty("value1"); + final AbstractIdentifiedType pt2 = resultType.getProperty("renamed1"); + final AbstractIdentifiedType pt3 = resultType.getProperty("computed"); + assertTrue(pt1 instanceof DefaultAttributeType<?>); + assertTrue(pt2 instanceof AbstractOperation); + assertTrue(pt3 instanceof AbstractOperation); + final AbstractIdentifiedType result2 = ((AbstractOperation) pt2).getResult(); + final AbstractIdentifiedType result3 = ((AbstractOperation) pt3).getResult(); + assertEquals(Integer.class, ((DefaultAttributeType<?>) pt1).getValueClass()); + assertTrue(result2 instanceof DefaultAttributeType<?>); + assertTrue(result3 instanceof DefaultAttributeType<?>); + assertEquals(Integer.class, ((DefaultAttributeType<?>) result2).getValueClass()); + assertEquals(String.class, ((DefaultAttributeType<?>) result3).getValueClass()); // Check feature instance. assertEquals(3, instance.getPropertyValue("value1")); @@@ -411,9 -454,10 +433,10 @@@ @Test public void testIncorrectVirtualProjection() throws DataStoreException { createFeaturesWithAssociation(); - final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); + final DefaultFilterFactory<AbstractFeature,?,?> ff = DefaultFilterFactory.forFeatures(); query.setProjection(new FeatureQuery.NamedExpression(ff.property("value1", Integer.class), (String) null), virtualProjection(ff.property("valueMissing", Integer.class), "renamed1")); + assertXPathsEqual("value1", "valueMissing"); var exception = assertThrows(DataStoreContentException.class, this::executeAndGetFirst); assertMessageContains(exception); diff --cc endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/image/WorldFileStoreTest.java index a5c8cdc1ee,0524750b93..8285116b5b --- a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/image/WorldFileStoreTest.java +++ b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/image/WorldFileStoreTest.java @@@ -100,10 -100,11 +100,9 @@@ public final class WorldFileStoreTest e */ assertEquals("gradient", store.getIdentifier().get().toString()); final Metadata metadata = store.getMetadata(); - final Identification id = getSingleton(metadata.getIdentificationInfo()); - final String format = getSingleton(id.getResourceFormats()).getFormatSpecificationCitation().getTitle().toString(); - assertTrue(format.contains("PNG"), format); + final DataIdentification id = (DataIdentification) getSingleton(metadata.getIdentificationInfo()); assertEquals("WGS 84", getSingleton(metadata.getReferenceSystemInfo()).getName().getCode()); - final GeographicBoundingBox bbox = (GeographicBoundingBox) - getSingleton(getSingleton(id.getExtents()).getGeographicElements()); + final var bbox = (GeographicBoundingBox) getSingleton(getSingleton(id.getExtents()).getGeographicElements()); assertEquals( -90, bbox.getSouthBoundLatitude()); assertEquals( +90, bbox.getNorthBoundLatitude()); assertEquals(-180, bbox.getWestBoundLongitude());