This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit 913e4ff28aa3b744c02a896e1873d4ce3500eb4e Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Mar 28 16:14:25 2025 +0100 Move `ListingPropertyVisitor` from Shapefile module to the main feature module. It will also be needed by the SQL data store, among others. --- .../sis/filter/privy/ListingPropertyVisitor.java | 137 +++++++++++++++++++++ .../main/org/apache/sis/storage/FeatureQuery.java | 24 ++++ .../org/apache/sis/storage/FeatureQueryTest.java | 24 ++++ .../storage/shapefile/ListingPropertyVisitor.java | 82 ------------ .../sis/storage/shapefile/ShapefileStore.java | 7 +- 5 files changed, 188 insertions(+), 86 deletions(-) diff --git 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 new file mode 100644 index 0000000000..4b9e664a31 --- /dev/null +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/privy/ListingPropertyVisitor.java @@ -0,0 +1,137 @@ +/* + * 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; + + +/** + * 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); + }); + 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) { + 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 --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java index 03c96f1d7e..675104c8c7 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/FeatureQuery.java @@ -36,6 +36,7 @@ import org.apache.sis.feature.privy.AttributeConvention; import org.apache.sis.feature.privy.FeatureExpression; import org.apache.sis.filter.DefaultFilterFactory; import org.apache.sis.filter.Optimization; +import org.apache.sis.filter.privy.ListingPropertyVisitor; import org.apache.sis.filter.privy.SortByComparator; import org.apache.sis.filter.privy.XPath; import org.apache.sis.storage.internal.Resources; @@ -674,6 +675,29 @@ public class FeatureQuery extends Query implements Cloneable, Serializable { return CharSequences.shortSentence(text, 40).toString(); } + /** + * Returns all XPaths used, directly or indirectly, by this query. + * The XPath values are extracted from all {@link ValueReference} expressions found in the + * {@linkplain #getSelection() selection} and in the {@linkplain #getProjection() projection}. + * The {@linkplain NamedExpression#alias aliases} are ignored. + * + * <p>The elements in the returned set are in no particular order. + * The set may be empty but never null.</p> + * + * @return all XPaths used, directly or indirectly, by this query. + * + * @since 1.5 + */ + public Set<String> getXPaths() { + Set<String> xpaths = ListingPropertyVisitor.xpaths(selection, null); + if (projection != null) { + for (NamedExpression e : projection) { + xpaths = ListingPropertyVisitor.xpaths(e.expression, xpaths); + } + } + return xpaths; + } + /** * Applies this query on the given feature set. * This method is invoked by the default implementation of {@link FeatureSet#subset(Query)}. diff --git 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 index c57fb684fc..6bd071f8f3 100644 --- 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,6 +33,7 @@ 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: @@ -57,6 +58,7 @@ import org.opengis.filter.SortProperty; * @author Alexis Manin (Geomatys) * @author Martin Desruisseaux (Geomatys) */ +@SuppressWarnings("exports") public final class FeatureQueryTest extends TestCase { /** * An arbitrary number of features, all of the same type. @@ -170,6 +172,15 @@ public final class FeatureQueryTest extends TestCase { } } + /** + * Verifies that the XPath set contains all the given elements. + * + * @param expected the expected XPaths. + */ + private void assertXPathsEqual(final String... expected) { + assertSetEquals(Arrays.asList(expected), query.getXPaths()); + } + /** * Verifies the effect of {@link FeatureQuery#setLimit(long)}. * @@ -179,6 +190,7 @@ public final class FeatureQueryTest extends TestCase { public void testLimit() throws DataStoreException { createFeaturesWithAssociation(); query.setLimit(2); + assertXPathsEqual(); verifyQueryResult(0, 1); } @@ -191,6 +203,7 @@ public final class FeatureQueryTest extends TestCase { public void testOffset() throws DataStoreException { createFeaturesWithAssociation(); query.setOffset(2); + assertXPathsEqual(); verifyQueryResult(2, 3, 4); } @@ -205,6 +218,7 @@ public final class FeatureQueryTest extends TestCase { final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); query.setSortBy(ff.sort(ff.property("value1", Integer.class), SortOrder.ASCENDING), ff.sort(ff.property("value2", Integer.class), SortOrder.DESCENDING)); + assertXPathsEqual(); verifyQueryResult(3, 1, 2, 0, 4); } @@ -219,6 +233,7 @@ public final class FeatureQueryTest extends TestCase { final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); query.setSelection(ff.equal(ff.property("value1", Integer.class), ff.literal(2), true, MatchAction.ALL)); + assertXPathsEqual("value1"); verifyQueryResult(1, 2); } @@ -233,6 +248,7 @@ public final class FeatureQueryTest extends TestCase { createFeaturesWithAssociation(); final FilterFactory<Feature,?,?> ff = DefaultFilterFactory.forFeatures(); query.setSelection(ff.equal(ff.property("dependency/value3"), ff.literal(18))); + assertXPathsEqual("dependency/value3"); verifyQueryResult(3); } @@ -248,6 +264,7 @@ public final class FeatureQueryTest extends TestCase { 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(); @@ -279,6 +296,7 @@ public final class FeatureQueryTest extends TestCase { public void testProjectionByNames() throws DataStoreException { createFeaturesWithAssociation(); query.setProjection("value2"); + assertXPathsEqual("value2"); final Feature instance = executeAndGetFirst(); final PropertyType p = TestUtilities.getSingleton(instance.getType().getProperties(true)); assertEquals("value2", p.getName().toString()); @@ -298,6 +316,7 @@ public final class FeatureQueryTest extends TestCase { 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(); @@ -322,6 +341,7 @@ public final class FeatureQueryTest extends TestCase { final FilterFactory<Feature,?,?> 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(); @@ -352,6 +372,7 @@ public final class FeatureQueryTest extends TestCase { final FilterFactory<Feature,?,?> 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(); assertEquals( 2, instance.getPropertyValue("value1")); @@ -368,6 +389,7 @@ public final class FeatureQueryTest extends TestCase { public void testProjectionOfLink() throws DataStoreException { createFeatureWithIdentifier(); query.setProjection(AttributeConvention.IDENTIFIER); + assertXPathsEqual(AttributeConvention.IDENTIFIER); final Feature instance = executeAndGetFirst(); assertEquals("id-0", instance.getPropertyValue(AttributeConvention.IDENTIFIER)); } @@ -392,6 +414,7 @@ public final class FeatureQueryTest extends TestCase { 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(); @@ -434,6 +457,7 @@ public final class FeatureQueryTest extends TestCase { final FilterFactory<Feature,?,?> 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 --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ListingPropertyVisitor.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ListingPropertyVisitor.java deleted file mode 100644 index bde71bcb45..0000000000 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ListingPropertyVisitor.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.shapefile; - -import java.util.Collection; -import org.apache.sis.filter.privy.FunctionNames; -import org.apache.sis.filter.privy.Visitor; - -// 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; - - -/** - * Expression visitor that returns a list of all Feature attributs requiered by this expression. - * - * @author Johann Sorel (Geomatys) - */ -final class ListingPropertyVisitor extends Visitor<Object,Collection<String>> { - - public static final ListingPropertyVisitor VISITOR = new ListingPropertyVisitor(); - - protected ListingPropertyVisitor() { - setLogicalHandlers((f, names) -> { - final LogicalOperator<Object> filter = (LogicalOperator<Object>) f; - for (Filter<Object> child : filter.getOperands()) { - visit(child, names); - } - }); - setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_BETWEEN), (f, names) -> { - final BetweenComparisonOperator<Object> 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 LikeOperator<Object> filter = (LikeOperator<Object>) f; - visit(filter.getExpressions().get(0), names); - }); - setExpressionHandler(FunctionNames.ValueReference, (e, names) -> { - final ValueReference<Object,?> expression = (ValueReference<Object,?>) e; - final String propName = expression.getXPath(); - if (!propName.trim().isEmpty()) { - names.add(propName); - } - }); - } - - @Override - protected void typeNotFound(final CodeList<?> type, final Filter<Object> filter, final Collection<String> names) { - for (final Expression<? super Object, ?> f : filter.getExpressions()) { - visit(f, names); - } - } - - @Override - protected void typeNotFound(final String type, final Expression<Object, ?> expression, final Collection<String> names) { - for (final Expression<? super Object, ?> p : expression.getParameters()) { - visit(p, names); - } - } -} diff --git a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java index 5015d89163..da6d5986a1 100644 --- a/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java +++ b/incubator/src/org.apache.sis.storage.shapefile/main/org/apache/sis/storage/shapefile/ShapefileStore.java @@ -28,7 +28,6 @@ import java.time.LocalDate; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; @@ -70,6 +69,7 @@ import org.apache.sis.feature.privy.AttributeConvention; import org.apache.sis.filter.DefaultFilterFactory; import org.apache.sis.filter.Optimization; import org.apache.sis.filter.privy.FunctionNames; +import org.apache.sis.filter.privy.ListingPropertyVisitor; import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.io.stream.ChannelDataInput; import org.apache.sis.io.stream.ChannelDataOutput; @@ -559,10 +559,9 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe boolean simpleSelection = true; //true if there are no alias and all expressions are ValueReference Set<String> properties = null; if (projection != null) { - properties = new HashSet<>(); - if (selection!=null) ListingPropertyVisitor.VISITOR.visit((Filter) selection, properties); + properties = ListingPropertyVisitor.xpaths(selection, properties); for (FeatureQuery.NamedExpression ne : projection) { - ListingPropertyVisitor.VISITOR.visit((Expression) ne.expression, properties); + properties = ListingPropertyVisitor.xpaths(ne.expression, properties); simpleSelection &= (ne.alias == null); simpleSelection &= (ne.expression.getFunctionName().tip().toString().equals(FunctionNames.ValueReference)); }