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 a3a0d404ad8046cf9ef132561efc9912a88e3125 Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri Dec 26 19:41:57 2025 +0100 When optimizing a query for a `FeatureType`, need to take in account that subtypes may exist. It forces us to consider all possible sub-types if they are known, or be conservative otherwise. --- .../main/org/apache/sis/feature/Features.java | 4 + .../internal/shared/FeatureProjectionBuilder.java | 5 +- .../org/apache/sis/filter/AssociationValue.java | 73 +++++--- .../apache/sis/filter/BinaryGeometryFilter.java | 23 +-- .../org/apache/sis/filter/IdentifierFilter.java | 16 +- .../main/org/apache/sis/filter/Optimization.java | 196 ++++++++++++++++++++- .../main/org/apache/sis/filter/PropertyValue.java | 101 ++++++----- .../org/apache/sis/filter/sqlmm/TwoGeometries.java | 30 +--- .../apache/sis/geometry/wrapper/Geometries.java | 13 ++ .../sis/geometry/wrapper/jts/FilteringContext.java | 2 +- .../org/apache/sis/filter/LogicalFilterTest.java | 2 +- .../apache/sis/filter/sqlmm/RegistryTestCase.java | 2 +- .../sis/storage/netcdf/base/DiscreteSampling.java | 13 ++ .../sis/storage/sql/feature/FeatureStream.java | 2 +- .../org/apache/sis/storage/sql/feature/Table.java | 11 ++ .../sql/feature/SelectionClauseWriterTest.java | 2 +- .../org/apache/sis/storage/AbstractFeatureSet.java | 31 +++- .../main/org/apache/sis/storage/FeatureQuery.java | 21 ++- .../main/org/apache/sis/storage/FeatureSubset.java | 17 ++ .../org/apache/sis/storage/MemoryFeatureSet.java | 135 ++++++++++++-- .../storage/aggregate/ConcatenatedFeatureSet.java | 25 ++- .../sis/storage/aggregate/JoinFeatureSet.java | 16 +- .../org/apache/sis/storage/internal/Resources.java | 5 + .../sis/storage/internal/Resources.properties | 1 + .../sis/storage/internal/Resources_fr.properties | 1 + .../main/org/apache/sis/util/Classes.java | 12 +- .../test/org/apache/sis/map/SEPortrayerTest.java | 2 +- .../sis/storage/shapefile/ShapefileStore.java | 14 +- .../org/apache/sis/storage/gdal/FeatureLayer.java | 12 ++ 29 files changed, 635 insertions(+), 152 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java index dc27495cde..429a40f3da 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/Features.java @@ -26,6 +26,7 @@ import org.opengis.metadata.quality.ConformanceResult; import org.opengis.metadata.quality.DataQuality; import org.opengis.metadata.quality.Element; import org.opengis.metadata.quality.Result; +import org.apache.sis.util.OptionalCandidate; import org.apache.sis.util.iso.Names; import org.apache.sis.util.iso.DefaultNameFactory; import org.apache.sis.feature.internal.Resources; @@ -209,6 +210,7 @@ public final class Features { * * @since 1.0 */ + @OptionalCandidate public static FeatureType findCommonParent(final Iterable<? extends FeatureType> types) { return (types != null) ? CommonParentFinder.select(types) : null; } @@ -240,6 +242,7 @@ public final class Features { * * @since 1.0 */ + @OptionalCandidate public static Class<?> getValueClass(PropertyType type) { while (type instanceof Operation) { final IdentifiedType result = ((Operation) type).getResult(); @@ -281,6 +284,7 @@ public final class Features { * * @since 0.8 */ + @OptionalCandidate public static GenericName getValueTypeName(final PropertyType property) { if (property instanceof FeatureAssociationRole) { // Tested first because this is the main interest for this method. diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java index 17ca838849..3f98d67641 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureProjectionBuilder.java @@ -213,7 +213,6 @@ public final class FeatureProjectionBuilder extends FeatureTypeBuilder { * In such case, the caller does not want the operation to be executed, since the property will rather * be used as a slot for receiving the result. * - * @param childType the feature type to use. * @param expression the expression from which to get the expected type. * @return handler for the property, or {@code null} if it cannot be resolved. */ @@ -464,6 +463,8 @@ public final class FeatureProjectionBuilder extends FeatureTypeBuilder { /** * Returns a string representation for debugging purposes. + * + * @return a string representation for debugging purposes. */ @Override public String toString() { @@ -757,7 +758,7 @@ public final class FeatureProjectionBuilder extends FeatureTypeBuilder { */ public Optional<FeatureProjection> project() { final var optimizer = new Optimization(); - optimizer.setFeatureType(source); + optimizer.setFinalFeatureType(source); requested.forEach((item) -> item.finish(optimizer)); /* * Add properties for all dependencies that are required by operations but are not already present. diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java index 8db223d154..bf68949e9f 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/AssociationValue.java @@ -16,6 +16,7 @@ */ package org.apache.sis.filter; +import java.util.ArrayList; import java.util.Arrays; import java.util.Set; import java.util.List; @@ -156,42 +157,60 @@ walk: if (instance != null) { /** * If at least one evaluated property is a link, replaces this expression by more direct references - * to the target properties. This is needed for better SQL WHERE clause in database queries. + * to the target properties. This is needed for better SQL {@code WHERE} clause in database queries. */ @Override public Expression<Feature, V> optimize(final Optimization optimization) { - final FeatureType specifiedType = optimization.getFeatureType(); -walk: if (specifiedType != null) try { - FeatureType type = specifiedType; - String[] direct = path; // To be cloned before any modification. - for (int i=0; i<path.length; i++) { - PropertyType property = type.getProperty(path[i]); - Optional<String> link = Features.getLinkTarget(property); - if (link.isPresent()) { - if (direct == path) direct = direct.clone(); - property = type.getProperty(direct[i] = link.get()); + final var actualTypes = new ArrayList<FeatureType>(); + final var pathInstances = new ArrayList<String[]>(2); + pathInstances.add(path); + try { + final String[] actualPath = optimization.constantResultForAllTypes((type) -> { + String[] realPath = path; // To be cloned before any modification. + for (int i=0; i<realPath.length; i++) { + PropertyType property = type.getProperty(realPath[i]); + Optional<String> link = Features.getLinkTarget(property); + if (link.isPresent()) { + if (realPath == path) realPath = realPath.clone(); + property = type.getProperty(realPath[i] = link.get()); + } + if (property instanceof FeatureAssociationRole) { + type = ((FeatureAssociationRole) property).getValueType(); + } else { + return null; + } } - if (!(property instanceof FeatureAssociationRole)) break walk; - type = ((FeatureAssociationRole) property).getValueType(); - } + actualTypes.add(type); + // Workaround needed because the `equals(…)` method of arrays performs an identity comparison. + for (final String[] canonical : pathInstances) { + if (Arrays.equals(canonical, realPath)) { + return canonical; + } + } + pathInstances.add(realPath); + return realPath; + }); /* * At this point all links have been resolved, up to the final property to evaluate. * Delegate the final property optimization to `accessor` which may not only resolve * links but also tune the `ObjectConverter`. */ - final Expression<Feature, V> converted; - optimization.setFeatureType(type); - try { - converted = accessor.optimize(optimization); - } finally { - optimization.setFeatureType(specifiedType); - } - if (converted != accessor || direct != path) { - if (converted instanceof PropertyValue<?>) { - return new AssociationValue<>(direct, (PropertyValue<V>) converted); - } else { - // If not a `PropertyValue`, then it should be a `Literal`. - return converted; + if (actualPath != null) { + final Expression<Feature, V> converted; + final Set<FeatureType> currentTypes = optimization.getFinalFeatureTypes(); + optimization.setFinalFeatureTypes(actualTypes); + try { + converted = accessor.optimize(optimization); + } finally { + optimization.setFinalFeatureTypes(currentTypes); + } + if (converted != accessor || actualPath != path) { + if (converted instanceof PropertyValue<?>) { + return new AssociationValue<>(actualPath, (PropertyValue<V>) converted); + } else { + // If not a `PropertyValue`, then it should be a `Literal`. + return converted; + } } } } catch (PropertyNotFoundException e) { diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java index 8df570ae0c..b8b50751c0 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java @@ -18,12 +18,10 @@ package org.apache.sis.filter; import java.util.List; import javax.measure.Unit; -import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.geometry.wrapper.GeometryWrapper; import org.apache.sis.geometry.wrapper.SpatialOperationContext; -import org.apache.sis.feature.internal.shared.AttributeConvention; import org.apache.sis.filter.base.Node; import org.apache.sis.util.Exceptions; @@ -31,11 +29,9 @@ import org.apache.sis.util.Exceptions; import org.opengis.filter.Filter; import org.opengis.filter.Literal; import org.opengis.filter.Expression; -import org.opengis.filter.ValueReference; import org.opengis.filter.SpatialOperator; import org.opengis.filter.BinarySpatialOperator; import org.opengis.filter.InvalidFilterValueException; -import org.opengis.feature.FeatureType; import org.opengis.feature.PropertyNotFoundException; @@ -218,18 +214,13 @@ abstract class BinaryGeometryFilter<R> extends Node implements SpatialOperator<R * then try to fetch the CRS of the property values. If we can transform the literal to that * CRS, do it now in order to avoid doing this transformation for all feature instances. */ - final FeatureType featureType = optimization.getFeatureType(); - if (featureType != null && other instanceof ValueReference<?,?>) try { - final CoordinateReferenceSystem targetCRS = AttributeConvention.getCRSCharacteristic( - featureType, featureType.getProperty(((ValueReference<?,?>) other).getXPath())); - if (targetCRS != null) { - final GeometryWrapper geometry = wrapper.apply(null); - final GeometryWrapper transformed = geometry.transform(targetCRS); - if (geometry != transformed) { - literal = Optimization.literal(transformed); - if (literal == effective1) effective1 = literal; - else effective2 = literal; - } + final GeometryWrapper geometry = wrapper.apply(null); + if (geometry != null) try { + final GeometryWrapper transformed = geometry.transform(optimization.findExpectedCRS(other).orElse(null)); + if (geometry != transformed) { + literal = Optimization.literal(transformed); + if (literal == effective1) effective1 = literal; + else effective2 = literal; } } catch (PropertyNotFoundException | TransformException e) { warning(e, true); diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java index c3439df00c..80b3ecb877 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/IdentifierFilter.java @@ -17,16 +17,15 @@ package org.apache.sis.filter; import java.util.List; +import java.util.HashSet; import java.util.Collection; import java.util.Objects; import org.apache.sis.filter.base.Node; import org.apache.sis.filter.base.XPathSource; -import org.apache.sis.feature.Features; import org.apache.sis.feature.internal.shared.AttributeConvention; // 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.Expression; import org.opengis.filter.ResourceId; @@ -84,12 +83,17 @@ final class IdentifierFilter extends Node */ @Override public Filter<Feature> optimize(Optimization optimization) { - final FeatureType type = optimization.getFeatureType(); - if (type != null) try { - return Features.getLinkTarget(type.getProperty(property)).map((n) -> new IdentifierFilter(this, n)).orElse(this); + final var found = new HashSet<String>(); + try { + final String preferredName = optimization.getPreferredPropertyName(property, found); + if (!preferredName.equals(property)) { + return new IdentifierFilter(this, preferredName); + } } catch (PropertyNotFoundException e) { warning(e, true); - return Filter.exclude(); + if (found.isEmpty()) { + return Filter.exclude(); // The property does not exist in any feature type. + } } return this; } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java index 3253ba51be..4b796616a9 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/Optimization.java @@ -19,10 +19,19 @@ package org.apache.sis.filter; import java.util.Map; import java.util.Set; import java.util.List; +import java.util.Iterator; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Collection; import java.util.IdentityHashMap; import java.util.ConcurrentModificationException; +import java.util.Optional; +import java.util.function.Function; import java.util.function.Predicate; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.feature.internal.shared.AttributeConvention; +import org.apache.sis.feature.Features; +import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.math.FunctionProperty; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.collection.Containers; @@ -33,9 +42,11 @@ import org.opengis.util.CodeList; import org.opengis.filter.Filter; import org.opengis.filter.Literal; import org.opengis.filter.Expression; +import org.opengis.filter.ValueReference; import org.opengis.filter.LogicalOperator; import org.opengis.filter.LogicalOperatorName; import org.opengis.feature.FeatureType; +import org.opengis.feature.PropertyNotFoundException; /** @@ -89,13 +100,15 @@ public class Optimization { private static final Object COMPUTING = Void.TYPE; /** - * The type of feature instances to be filtered, or {@code null} if unknown. + * Exhaustive set of types of all feature instances that the filters and expressions may see. + * This is an empty set if the feature types are unknown or irrelevant for the type of resources to be filtered. */ - private FeatureType featureType; + private Set<FeatureType> featureTypes; /** * Filters and expressions already optimized. Also used for avoiding never-ending loops. - * The map is created when first needed. + * The map is created when first needed. The null value (not the same as an empty map) is + * used for identifying the start of recursive invocations of {@code apply(…)} methods. * * <h4>Implementation note</h4> * The same map is used for filters and expressions. @@ -103,13 +116,17 @@ public class Optimization { * If it happens anyway, it should still be okay because the method signatures are * the same in both interfaces (only the return type changes), so the same methods * would be invoked no matter if we consider the keys as a filter or an expression. + * + * @see #apply(Filter) + * @see #apply(Expression) */ - private Map<Object,Object> done; + private Map<Object, Object> done; /** * Creates a new instance. */ public Optimization() { + featureTypes = Set.of(); } /** @@ -118,9 +135,12 @@ public class Optimization { * The default value is {@code null}. * * @return the type of feature instances to be filtered, or {@code null} if unknown. + * + * @deprecated Replaced by {@link #getFinalFeatureTypes()}. */ + @Deprecated(since = "1.6", forRemoval = true) public FeatureType getFeatureType() { - return featureType; + return Containers.peekIfSingleton(getFinalFeatureTypes()); } /** @@ -130,9 +150,173 @@ public class Optimization { * in advance. * * @param type the type of feature instances to be filtered, or {@code null} if unknown. + * + * @deprecated Replaced by {@link #setFinalFeatureTypes(Collection)}. */ + @Deprecated(since = "1.6", forRemoval = true) public void setFeatureType(final FeatureType type) { - featureType = type; + setFinalFeatureType(type); + } + + /** + * Returns the exhaustive set of the types of all feature instances that the filters and expressions may see. + * The super-types should not be included in the set, unless some features may be instances of these specific + * super-types rather than instances of a some sub-type. If the set of feature types is unknown or irrelevant + * for the type of resources to be filtered, then this method returns an empty set. + * + * <h4>Purpose</h4> + * A {@link org.apache.sis.storage.DataStore} may contain a hierarchy of feature types instead of a single type. + * A property may be absent in the parent type but present in some sub-types, or may be overridden in sub-types. + * If an optimization wants to evaluate once and for all an expression with literal parameters, the optimization + * needs to verify that the parameters are really literals in all possible sub-types. + * + * @return exhaustive set of types of all feature instances that the filters and expressions may see. + * + * @since 1.6 + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + public Set<FeatureType> getFinalFeatureTypes() { + return featureTypes; + } + + /** + * Specifies the exhaustive set of the types of all feature instances that the filters and expressions may see. + * The given collection should not include super-types, unless some features may be instances of these specific + * super-types rather than instances of a some sub-type. An empty collection means that feature types are unknown + * or irrelevant for the type of resources to be filtered. + * + * @param types exhaustive set of types of all feature instances that the filters and expressions may see. + * + * @since 1.6 + */ + public void setFinalFeatureTypes(final Collection<? extends FeatureType> types) { + featureTypes = Set.copyOf(types); + } + + /** + * Specifies the single type of all feature instances that the filters and expressions may see. + * This is a convenience method delegating to {@link #setFinalFeatureTypes(Collection)} with a + * singleton or empty set. + * + * <p>Note that the given type should be effectively final, i.e. no subtype should exist. + * If the feature instances may be of some subtypes, then all subtypes should be enumerated + * in a call to {@link #setFinalFeatureTypes(Collection)}.</p> + * + * @param type the type of feature instances to be filtered, or {@code null} if unknown. + * + * @since 1.6 + */ + public final void setFinalFeatureType(final FeatureType type) { + setFinalFeatureTypes((type != null) ? Set.of(type) : Set.of()); + } + + /** + * If the result of applying the given function is equal for all feature types, returns that value. + * Otherwise, returns {@code null}. This is used for implementation of {@code optimize(…)} methods. + * + * @param <R> type of the result. + * @param mapper the operation to apply on each feature type. + * @return the constant result, or {@code null} if none. + */ + final <R> R constantResultForAllTypes(final Function<FeatureType, R> mapper) { + final Iterator<FeatureType> it = getFinalFeatureTypes().iterator(); + if (it.hasNext()) { + final R value = mapper.apply(it.next()); + if (value != null) { + while (it.hasNext()) { + if (!value.equals(mapper.apply(it.next()))) { + return null; + } + } + return value; + } + } + return null; + } + + /** + * Fetches the real name of the given property after resolution of links in all feature types. + * The real name depends on the feature types declared by {@link #getFinalFeatureTypes()}. + * If the specified property is present in all declared feature types and all these properties + * are links referencing the same target property, then this method returns that target property. + * Otherwise, this method returns {@code property}. + * + * <p>If at least one feature type does not have the requested property, then an exception is thrown. + * This method finished the iteration over all types before to throw {@link PropertyNotFoundException}. + * Therefore, the size of {@code addTo} can be used for detecting if at least one feature type has the + * property. If {@code addTo} is empty, then the property has not been found in any feature type.</p> + * + * @param property name of the property to resolve. + * @param addTo where to add the XPath of the specified property for all feature types. + * @return preferred name to use for fetching the property values for all feature types. + * @throws PropertyNotFoundException if at least one feature type does not have the specified property. + */ + @SuppressWarnings("ThrowableResultIgnored") + final String getPreferredPropertyName(final String property, final Set<String> addTo) throws PropertyNotFoundException { + final var exceptions = new HashMap<FeatureType, PropertyNotFoundException>(); + for (final FeatureType type : getFinalFeatureTypes()) { + try { + addTo.add(Features.getLinkTarget(type.getProperty(property)).orElse(property)); + } catch (PropertyNotFoundException e) { + exceptions.putIfAbsent(type, e); + } + } + if (exceptions.isEmpty()) { + final String name = Containers.peekIfSingleton(addTo); + return (name != null) ? name : property; + } + /* + * Throws the exception associated with the most basic feature type. + * The base type search is not mandatory, but provide more useful stack trace. + */ + final PropertyNotFoundException e = valueOfBaseType(exceptions); + while (!exceptions.isEmpty()) { + e.addSuppressed(valueOfBaseType(exceptions)); + } + throw e; + } + + /** + * Returns the value associated to the base type among all keys of the given map. + * If no base type is found, then an arbitrary entry is used. + * This method always removes exactly one entry from the map. + */ + private static <E> E valueOfBaseType(final Map<FeatureType, E> map) { + E e = map.remove(Features.findCommonParent(map.keySet())); + if (e == null) { + Iterator<E> it = map.values().iterator(); + e = it.next(); + it.remove(); + } + return e; + } + + /** + * If the specified parameter should always use the same Coordinate Reference System, returns that <abbr>CRS</abbr>. + * The {@code parameter} argument is usually one of the elements returned by {@link Expression#getParameters()} or + * {@link Filter#getExpressions()}, and the <abbr>CRS</abbr> used by that parameter may depend on the feature types + * declared by {@link #getFinalFeatureTypes()}. + * The returned value is empty if the <abbr>CRS</abbr> is unknown or not the same for all feature types. + * + * @param parameter a parameter of a filter or expression. + * @return the <abbr>CRS</abbr> expected for the specified parameter, or empty if unknown of not unique. + * @throws PropertyNotFoundException if the parameter is a {@link ValueReference} and + * the referenced property has not been found in at least one feature type. + * + * @since 1.6 + */ + public Optional<CoordinateReferenceSystem> findExpectedCRS(final Expression<?,?> parameter) + throws PropertyNotFoundException + { + CoordinateReferenceSystem crs = null; + if (parameter instanceof Literal<?,?>) { + crs = Geometries.getCoordinateReferenceSystem(((Literal<?,?>) parameter).getValue()); + } else if (parameter instanceof ValueReference<?,?>) { + final String xpath = ((ValueReference<?,?>) parameter).getXPath(); + crs = constantResultForAllTypes( + (type) -> AttributeConvention.getCRSCharacteristic(type, type.getProperty(xpath))); + } + return Optional.ofNullable(crs); } /** diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java index f3a521ddd1..b5e930790a 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/PropertyValue.java @@ -17,20 +17,22 @@ package org.apache.sis.filter; import java.util.List; +import java.util.HashSet; +import java.util.HashMap; import java.util.Optional; import java.util.Collection; import org.apache.sis.feature.Features; import org.apache.sis.util.ObjectConverter; import org.apache.sis.util.ObjectConverters; import org.apache.sis.util.UnconvertibleObjectException; +import org.apache.sis.util.collection.Containers; +import org.apache.sis.util.resources.Errors; import org.apache.sis.feature.internal.shared.FeatureProjectionBuilder; import org.apache.sis.filter.base.XPath; import org.apache.sis.filter.base.XPathSource; -import org.apache.sis.util.resources.Errors; // Specific to the geoapi-3.1 and geoapi-4.0 branches: import org.opengis.feature.Feature; -import org.opengis.feature.FeatureType; import org.opengis.feature.PropertyType; import org.opengis.feature.AttributeType; import org.opengis.feature.PropertyNotFoundException; @@ -260,13 +262,17 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V> */ @Override public Expression<Feature, Object> optimize(final Optimization optimization) { - final FeatureType type = optimization.getFeatureType(); - if (type != null) try { - return Features.getLinkTarget(type.getProperty(name)) - .map((rename) -> new AsObject(rename, isVirtual)).orElse(this); + final var found = new HashSet<String>(); + try { + final String preferredName = optimization.getPreferredPropertyName(name, found); + if (!preferredName.equals(name)) { + return new AsObject(preferredName, isVirtual); + } } catch (PropertyNotFoundException e) { warning(e, true); - return NULL(); + if (found.isEmpty()) { + return NULL(); // The property does not exist in any feature type. + } } return this; } @@ -331,47 +337,58 @@ abstract class PropertyValue<V> extends LeafExpression<Feature,V> */ @Override public final Expression<Feature, V> optimize(final Optimization optimization) { - final FeatureType featureType = optimization.getFeatureType(); - if (featureType != null) try { - /* - * Resolve link (e.g. "sis:identifier" as a reference to the real identifier property). - * This is important for allowing `SQLStore` to use the property in SQL WHERE statements. - * If there is no renaming to apply (which is the usual case), then `rename` is null. - */ - String rename = name; - PropertyType property = featureType.getProperty(rename); + /* + * Resolve links (e.g. "sis:identifier" as a reference to the real identifier property). + * This is important for allowing `SQLStore` to use the property in SQL WHERE statements. + */ + final var found = new HashSet<String>(); + final String preferredName; + try { + preferredName = optimization.getPreferredPropertyName(name, found); + } catch (PropertyNotFoundException e) { + warning(e, true); + return found.isEmpty() ? NULL() : this; + } + /* + * Check if the same converter can be used for all feature types. + * This is guaranteed to be true if the requested property contains + * the same class of values in all feature types. + */ + final ObjectConverter<?, ? extends V> converter; + final var actualTypes = new HashMap<Class<?>, ObjectConverter<?, ? extends V>>(); + converter = optimization.constantResultForAllTypes((featureType) -> { + PropertyType property = featureType.getProperty(preferredName); Optional<String> target = Features.getLinkTarget(property); - if (target.isPresent()) try { - rename = target.get(); - property = featureType.getProperty(rename); - } catch (PropertyNotFoundException e) { - warning(e, true); - rename = name; + if (target.isPresent()) { + property = featureType.getProperty(target.get()); } - /* - * At this point we did our best effort for having the property as an attribute, - * which allows us to get the expected type. If the type is not `Object`, we can - * try to fetch a more specific converter than the default `Converted` one. - */ - Class<?> source = getSourceClass(); - final Class<?> original = source; + final Class<?> source; if (property instanceof AttributeType<?>) { source = ((AttributeType<?>) property).getValueClass(); + } else { + source = getSourceClass(); } - if (!(rename.equals(name) && source.equals(original))) { - if (source == Object.class) { - return new Converted<>(type, rename, isVirtual); - } else if (type.isAssignableFrom(source)) { - return new Unsafe<>(source, type, rename, isVirtual); - } else { - return new CastedAndConverted<>(source, type, rename, isVirtual); - } - } - } catch (PropertyNotFoundException e) { - warning(e, true); - return NULL(); + return actualTypes.computeIfAbsent(source, (s) -> ObjectConverters.find(s, type)); + }); + /* + * Finished to collect the class of values in declared feature types. The `actualTypes` may contain more + * than one element if different feature types define the same property with values of different types, + * but a unique converter was nevertheless found. + */ + Class<?> source = Containers.peekIfSingleton(actualTypes.keySet()); + if (converter == null || (preferredName.equals(name) && (source == null || source == getSourceClass()))) { + return this; + } + if (source == null) { + source = converter.getSourceClass(); + } + if (source == Object.class) { + return new Converted<>(type, preferredName, isVirtual); + } else if (type.isAssignableFrom(source)) { + return new Unsafe<>(source, type, preferredName, isVirtual); + } else { + return new CastedAndConverted<>(source, type, preferredName, isVirtual); } - return this; } /** diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java index 81b49eaf28..d66ebf656b 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java @@ -17,19 +17,15 @@ package org.apache.sis.filter.sqlmm; import java.util.List; -import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.operation.TransformException; import org.apache.sis.filter.Optimization; -import org.apache.sis.feature.internal.shared.AttributeConvention; import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.geometry.wrapper.GeometryWrapper; // Specific to the geoapi-3.1 and geoapi-4.0 branches: -import org.opengis.feature.FeatureType; import org.opengis.feature.PropertyNotFoundException; import org.opengis.filter.Expression; import org.opengis.filter.Literal; -import org.opengis.filter.ValueReference; /** @@ -78,23 +74,15 @@ class TwoGeometries<R> extends SpatialFunction<R> { */ @Override public Expression<R,?> optimize(final Optimization optimization) { - final FeatureType featureType = optimization.getFeatureType(); - if (featureType != null) { - final Expression<R,?> p1 = unwrap(geometry1); - if (p1 instanceof ValueReference<?,?> && unwrap(geometry2) instanceof Literal<?,?>) try { - final CoordinateReferenceSystem targetCRS = AttributeConvention.getCRSCharacteristic( - featureType, featureType.getProperty(((ValueReference<?,?>) p1).getXPath())); - if (targetCRS != null) { - final GeometryWrapper literal = geometry2.apply(null); - if (literal != null) { - final GeometryWrapper tr = literal.transform(targetCRS); - if (tr != literal) { - @SuppressWarnings({"unchecked","rawtypes"}) - final Expression<R,?>[] effective = getParameters().toArray(Expression[]::new); - effective[1] = Optimization.literal(tr); - return recreate(effective); - } - } + if (unwrap(geometry2) instanceof Literal<?,?>) { + final GeometryWrapper literal = geometry2.apply(null); + if (literal != null) try { + final GeometryWrapper transformed = literal.transform(optimization.findExpectedCRS(unwrap(geometry1)).orElse(null)); + if (transformed != literal) { + @SuppressWarnings({"unchecked","rawtypes"}) + final Expression<R,?>[] effective = getParameters().toArray(Expression[]::new); + effective[1] = Optimization.literal(transformed); + return recreate(effective); } } catch (PropertyNotFoundException | TransformException e) { warning(e, true); diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java index 120a5a34ea..48e6b337cf 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/Geometries.java @@ -233,6 +233,19 @@ public abstract class Geometries<G> implements Serializable { return wrapper.implementation(); } + /** + * Returns the coordinate reference system of the given geometry, or {@code null} if none. + * This is a convenience method for cases where the <abbr>CRS</abbr> is the only desired information. + * If more information are needed, use {@link #wrap(Object)} instead. + * + * @param geometry the geometry instance (can be {@code null}). + * @return the coordinate reference system, or {@code null}. + * @throws BackingStoreException if the operation failed because of a checked exception. + */ + public static CoordinateReferenceSystem getCoordinateReferenceSystem(final Object geometry) { + return wrap(geometry).map(GeometryWrapper::getCoordinateReferenceSystem).orElse(null); + } + /** * Wraps the geometry stored in a property of the given feature. This method should be used * instead of {@link #wrap(Object)} when the value come from a feature instance in order to diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/FilteringContext.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/FilteringContext.java index ecf02b8394..71a7c24206 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/FilteringContext.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/FilteringContext.java @@ -32,7 +32,7 @@ import org.apache.sis.system.DelayedRunnable; * * <p>Ideally this object should be created when a filtering operation on a collection of features * is about to start, and disposed after the filtering operation is completed. We do not yet have - * a notification mechanism for those events, so current implementation use a {@link ThreadLocal}. + * a notification mechanism for those events, so current implementation use a {@link Queue}. * A future version may revisit this strategy and expand the use of "filtering context" to all * geometry implementations, not only JTS (but we may keep a specialized JTS subclass).</p> * diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java index e4dade8c0e..8106553156 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LogicalFilterTest.java @@ -253,7 +253,7 @@ public final class LogicalFilterTest extends TestCase { * Notify the optimizer that property values will be of `String` type. * The optimizer should compute an `ObjectConverter` in advance. */ - optimization.setFeatureType(feature); + optimization.setFinalFeatureType(feature); final var optimized = optimization.apply(expression); assertEquals(200, expression.apply(instance).intValue()); assertNotSame(expression, optimized); diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java index ab5ecd5c0f..533ff4b75c 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/RegistryTestCase.java @@ -524,7 +524,7 @@ public abstract class RegistryTestCase<G> extends TestCaseWithLogs { final var optimization = new Optimization(); final var ftb = new FeatureTypeBuilder(); ftb.addAttribute(library.pointClass).setName(P_NAME).setCRS(HardCodedCRS.WGS84); - optimization.setFeatureType(ftb.setName("Test").build()); + optimization.setFinalFeatureType(ftb.setName("Test").build()); final var optimized = optimization.apply(function); assertNotSame(function, optimized, "Optimization should produce a new expression."); /* diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/DiscreteSampling.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/DiscreteSampling.java index 9d2d6dcf42..940dcd9e52 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/DiscreteSampling.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/DiscreteSampling.java @@ -18,8 +18,11 @@ package org.apache.sis.storage.netcdf.base; import org.apache.sis.setup.GeometryLibrary; import org.apache.sis.geometry.wrapper.Geometries; +import org.apache.sis.filter.Optimization; import org.apache.sis.storage.DataStore; import org.apache.sis.storage.AbstractFeatureSet; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.FeatureQuery; import org.apache.sis.storage.base.StoreResource; import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.util.resources.Errors; @@ -86,6 +89,16 @@ public abstract class DiscreteSampling extends AbstractFeatureSet implements Sto return lock; } + /** + * Configures the optimization of a query with the knowledge that the feature type is final. + * This configuration asserts that all features will be instances of the type returned by + * {@link #getType()}, with no sub-type. + */ + @Override + protected final void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException { + optimizer.setFinalFeatureType(getType()); + } + /** * Returns the error message for a file that cannot be read. * diff --git 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 index 77e62ca76a..8cd7d05a69 100644 --- 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 @@ -196,7 +196,7 @@ final class FeatureStream extends DeferredStream<Feature> { return execute(() -> { Stream<Feature> stream = this; final var optimization = new Optimization(); - optimization.setFeatureType(table.featureType); + optimization.setFinalFeatureType(table.featureType); for (final var filter : optimization.applyAndDecompose((Filter<? super Feature>) predicate)) { if (filter == Filter.include()) continue; if (filter == Filter.exclude()) return empty(); diff --git 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 index c6a9fec04e..0588106972 100644 --- 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 @@ -29,8 +29,10 @@ import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import org.opengis.util.GenericName; import org.opengis.geometry.Envelope; +import org.apache.sis.filter.Optimization; import org.apache.sis.filter.InvalidXPathException; import org.apache.sis.filter.base.XPath; +import org.apache.sis.storage.FeatureQuery; import org.apache.sis.storage.AbstractFeatureSet; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.InternalDataStoreException; @@ -605,4 +607,13 @@ final class Table extends AbstractFeatureSet { public Stream<Feature> features(final boolean parallel) { return new FeatureStream(this, parallel); } + + /** + * Configures the optimization of a query with the knowledge that the feature type is final. + * This configuration asserts that all features will be instances of {@link #featureType} with no sub-type. + */ + @Override + protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException { + optimizer.setFinalFeatureType(featureType); + } } diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java index d9919b26ae..5e58eb45e5 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/feature/SelectionClauseWriterTest.java @@ -131,7 +131,7 @@ public final class SelectionClauseWriterTest extends TestCase implements SchemaM Filter<Feature> filter = FF.intersects(FF.property("BETA"), FF.literal(bbox)); final var optimization = new Optimization(); - optimization.setFeatureType(table.featureType); + optimization.setFinalFeatureType(table.featureType); verifySQL(optimization.apply(filter), "ST_Intersects(\"BETA\", " + "ST_GeomFromText('POLYGON ((20 -10, 25 -10, 25 -5, 20 -5, 20 -10))'))"); } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractFeatureSet.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractFeatureSet.java index 102d8f179e..d9cf12ac8a 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractFeatureSet.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/AbstractFeatureSet.java @@ -25,6 +25,7 @@ import org.opengis.metadata.Metadata; import org.apache.sis.storage.event.StoreListeners; import org.apache.sis.storage.base.MetadataBuilder; import org.apache.sis.storage.base.WarningAdapter; +import org.apache.sis.filter.Optimization; import org.apache.sis.filter.base.WarningEvent; // Specific to the geoapi-3.1 and geoapi-4.0 branches: @@ -49,7 +50,7 @@ import org.opengis.feature.FeatureType; * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @version 1.6 * @since 1.2 */ public abstract class AbstractFeatureSet extends AbstractResource implements FeatureSet { @@ -127,6 +128,32 @@ public abstract class AbstractFeatureSet extends AbstractResource implements Fea } } + /** + * Configures the optimization of a query to be applied on this {@code FeatureSet}. + * This method is invoked indirectly by the default implementation of {@link #subset(Query)}. + * The default implementation of this method does nothing. + * + * <h4>Recommendation</h4> + * Subclasses should override this method as below if they can guarantee that the feature type returned + * by {@link #getType()} is final, i.e. that the result of the query will not contain any feature which + * (before {@linkplain FeatureQuery#getProjection() projection}) is an instance of some subtype: + * + * {@snippet lang="java" : + * @Override + * protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException { + * optimizer.setFinalFeatureType(getType()); + * } + * } + * + * @param query definition of feature and feature properties filtering applied at reading time. + * @param optimizer the optimization to configure. + * @throws DataStoreException if an error occurred during the configuration of the optimizer. + * + * @since 1.6 + */ + protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException { + } + /** * Invoked in a synchronized block the first time that {@code getMetadata()} is invoked. * The default implementation populates metadata based on information provided by @@ -142,7 +169,7 @@ public abstract class AbstractFeatureSet extends AbstractResource implements Fea */ @Override protected Metadata createMetadata() throws DataStoreException { - final MetadataBuilder builder = new MetadataBuilder(); + final var builder = new MetadataBuilder(); builder.addDefaultMetadata(this, listeners); return builder.build(); } 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 9ee1019a4e..24a06a4ec4 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 @@ -83,7 +83,7 @@ import org.opengis.filter.SortProperty; * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.5 + * @version 1.6 * @since 1.1 */ public class FeatureQuery extends Query implements Cloneable, Emptiable, Serializable { @@ -641,6 +641,8 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali * The literal, value reference or more complex expression to be retrieved by a {@code Query}. * Never {@code null}. * + * @return the expression (often a literal) for the value to retrieve. + * * @since 1.5 */ public Expression<? super Feature, ?> expression() { @@ -650,6 +652,8 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali /** * The name to assign to the expression result, or {@code null} if unspecified. * + * @return optional name for the expression result. + * * @since 1.5 */ public GenericName alias() { @@ -662,6 +666,8 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali * a feature {@link Operation}. The latter are commonly called "computed fields" and are equivalent * to SQL {@code GENERATED ALWAYS} keyword for columns. * + * @return the projection type (stored or computing). + * * @since 1.5 */ public ProjectionType type() { @@ -841,12 +847,18 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali * @throws DataStoreException if an error occurred during the optimization of this query. * * @since 1.5 + * + * @deprecated Moved to {@link AbstractFeatureSet#prepareQueryOptimization(FeatureQuery, Optimization)} + * because experience suggests that this is the class that know best how to configure. */ + @Deprecated(since = "1.6", forRemoval = true) protected void optimize(final FeatureSet source) throws DataStoreException { if (selection != null) { - final var optimization = new Optimization(); - optimization.setFeatureType(source.getType()); - selection = optimization.apply(selection); + final var optimizer = new Optimization(); + if (source instanceof AbstractFeatureSet) { + ((AbstractFeatureSet) source).prepareQueryOptimization(this, optimizer); + } + selection = optimizer.apply(selection); } } @@ -856,7 +868,6 @@ public class FeatureQuery extends Query implements Cloneable, Emptiable, Seriali * @return a clone of this query. * * @see #FeatureQuery(FeatureQuery) - * @see #optimize(FeatureSet) */ @Override public FeatureQuery clone() { diff --git 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 index da64b8dac7..df9e4468e4 100644 --- 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 @@ -19,6 +19,7 @@ package org.apache.sis.storage; import java.util.OptionalLong; import java.util.stream.Stream; import org.opengis.metadata.Metadata; +import org.apache.sis.filter.Optimization; import org.apache.sis.feature.internal.shared.FeatureProjection; import org.apache.sis.storage.base.MetadataBuilder; import org.apache.sis.storage.base.StoreUtilities; @@ -73,6 +74,22 @@ final class FeatureSubset extends AbstractFeatureSet { this.query = query; } + /** + * Configures the optimization of a query for the expected types of all feature instances. + * If this subset is the result of a projection (in <abbr>SQL</abbr> sense), then all features + * are instances of {@link #getType()}. Otherwise, features are instances of the same type as + * in the original {@linkplain #source}, therefore the same optimization as the source is applied. + */ + @Override + protected void prepareQueryOptimization(final FeatureQuery query, final Optimization optimizer) throws DataStoreException { + final FeatureType type = getType(); + if (projection != null) { + optimizer.setFinalFeatureType(type); + } else if (source instanceof AbstractFeatureSet) { + ((AbstractFeatureSet) source).prepareQueryOptimization(query, optimizer); + } + } + /** * Creates metadata about this subset. * It includes information about the complete feature set. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/MemoryFeatureSet.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/MemoryFeatureSet.java index a2d8297510..9503679751 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/MemoryFeatureSet.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/MemoryFeatureSet.java @@ -16,10 +16,16 @@ */ package org.apache.sis.storage; +import java.util.Set; +import java.util.HashSet; import java.util.Objects; import java.util.Collection; import java.util.OptionalLong; import java.util.stream.Stream; +import org.apache.sis.feature.Features; +import org.apache.sis.filter.Optimization; +import org.apache.sis.storage.internal.Resources; +import org.apache.sis.util.resources.Errors; // Specific to the geoapi-3.1 and geoapi-4.0 branches: import org.opengis.feature.Feature; @@ -36,6 +42,15 @@ import org.opengis.feature.FeatureType; * or when the features are in memory anyway (for example, a computation result). * It should generally not be used for large data sets read from files or databases. * + * <h2>Mutability</h2> + * By default, the feature collection given at construction time is assumed stable. + * If the content of that collection is modified, then the {@link #refresh()} method + * should be invoked for rebuilding the {@linkplain #allTypes set of feature types}. + * + * <h2>Thread-safety</h2> + * This class is thread-safe if the collection given at construction time is thread-safe. + * Synchronizations use the lock returned by {@link #getSynchronizationLock()}. + * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) * @@ -43,43 +58,121 @@ import org.opengis.feature.FeatureType; */ public class MemoryFeatureSet extends AbstractFeatureSet { /** - * The type specified at construction time. + * The base type of all feature instances that the collection can contain. + * All elements of {@link #allTypes} must be assignable to this base type. * * @see #getType() */ - protected final FeatureType type; + protected final FeatureType baseType; + + /** + * The types, including sub-types, of all feature instances found in the collection. + * This is often a singleton containing only {@link #baseType}, but it may also be a + * set without {@code baseType} if all features are instances of various subtypes. + * + * <p>This set is modifiable and is updated when {@link #refresh()} is invoked. + * This set should always contains at least one element.</p> + * + * @see #refresh() + * @see #prepareQueryOptimization(FeatureQuery, Optimization) + */ + protected final Set<FeatureType> allTypes; /** * The features specified at construction time, potentially as a modifiable collection. - * For all features in this collection, {@link Feature#getType()} shall be {@link #type}. + * For all feature instances in this collection, the value returned by {@link Feature#getType()} + * shall be {@link #baseType} or a subtype of {@code baseType}. * * @see #features(boolean) */ protected final Collection<Feature> features; /** - * Creates a new set of features stored in memory. It is caller responsibility to ensure that - * <code>{@linkplain Feature#getType()} == type</code> for all elements in the given collection - * (this is not verified by this constructor). + * Creates a new set of feature instances stored in memory. + * The base feature type is determined automatically from the given collection of features. + * The collection shall contain at least one element. * - * @param parent the parent resource, or {@code null} if none. - * @param type the type of all features in the given collection. - * @param features collection of stored features. This collection will not be copied. + * @param features collection of stored features. This collection will not be copied. + * @throws IllegalArgumentException if the given collection is empty or does not contain + * feature instances having a common parent type. */ - public MemoryFeatureSet(final Resource parent, final FeatureType type, final Collection<Feature> features) { + public MemoryFeatureSet(Collection<Feature> features) { + this(null, null, features); + } + + /** + * Creates a new set of feature instances stored in memory with specified parent resource and base type. + * This constructor verifies that all feature instances are assignable to a base type. + * That base type can be either specified explicitly or inferred automatically. + * + * @param parent the parent resource, or {@code null} if none. + * @param baseType the base type of all features in the given collection, or {@code null} for automatic. + * @param features collection of stored features. This collection will not be copied. + * @throws IllegalArgumentException if {@code baseType} is null and cannot be determined from the feature instances, + * or if some feature instances are not assignable to {@code baseType}. + */ + public MemoryFeatureSet(final Resource parent, final FeatureType baseType, final Collection<Feature> features) { super(parent); - this.type = Objects.requireNonNull(type); this.features = Objects.requireNonNull(features); + allTypes = new HashSet<>(); + if (baseType != null) { + verifyFeatureInstances(baseType, true); + } else { + features.forEach((instance) -> allTypes.add(instance.getType())); + if (allTypes.isEmpty()) { + throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "features")); + } + } + if ((this.baseType = Features.findCommonParent(allTypes)) == null) { + throw new IllegalArgumentException(Resources.format(Resources.Keys.NoCommonFeatureType)); + } + } + + /** + * Scans all feature instances for building the set of feature types. + * This method opportunistically verifies that all instances are assignable to {@code baseType}. + * + * @param baseType tentative value of {@link #baseType}, may be provided before {@link #baseType} is set. + * @param creating whether this method is invoked from the constructor. + */ + private void verifyFeatureInstances(final FeatureType baseType, final boolean creating) { + for (final Feature feature : features) { + final FeatureType type = feature.getType(); + if (allTypes.add(type) && !baseType.isAssignableFrom(type)) { + allTypes.clear(); + String message = Resources.format(Resources.Keys.FeatureNotAssignableToBaseType_2, baseType.getName(), type.getName()); + throw creating ? new IllegalArgumentException(message) : new IllegalStateException(message); + } + } + if (allTypes.isEmpty()) { + allTypes.add(baseType); + } + } + + /** + * Notifies this {@code FeatureSet} that the elements in the collection of features have changed. + * This method re-verifies that all feature instances are assignable to the {@link #baseType} and + * rebuilds the set of all feature types. + * + * @throws IllegalStateException if some features are not instances of {@link #baseType}. + */ + public void refresh() { + synchronized (getSynchronizationLock()) { + allTypes.clear(); + verifyFeatureInstances(baseType, false); + } } /** * Returns the type common to all feature instances in this set. + * By default, this type is determined at construction time and does + * not change even if the content of the feature collection is updated. * * @return a description of properties that are common to all features in this dataset. */ @Override public FeatureType getType() { - return type; + return baseType; } /** @@ -103,6 +196,20 @@ public class MemoryFeatureSet extends AbstractFeatureSet { return parallel ? features.parallelStream() : features.stream(); } + /** + * Configures the optimization of a query with information about the expected types of all feature instances. + * This method is invoked indirectly when a {@linkplain #subset feature subset} is created from a query. + * The optimization depends on the set of {@linkplain #allTypes all feature types} found in the collection. + * + * @since 1.6 + */ + @Override + protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException { + synchronized (getSynchronizationLock()) { + optimizer.setFinalFeatureTypes(allTypes); + } + } + /** * Tests whether this memory feature set is wrapping the same feature instances as the given object. * This method checks also that the listeners are equal. @@ -114,7 +221,7 @@ public class MemoryFeatureSet extends AbstractFeatureSet { public boolean equals(final Object obj) { if (obj != null && obj.getClass() == getClass()) { final var other = (MemoryFeatureSet) obj; - return type.equals(other.type) && + return baseType.equals(other.baseType) && features.equals(other.features) && listeners.equals(other.listeners); } @@ -128,6 +235,6 @@ public class MemoryFeatureSet extends AbstractFeatureSet { */ @Override public int hashCode() { - return type.hashCode() + 31 * features.hashCode() + 37 * listeners.hashCode(); + return baseType.hashCode() + 31 * features.hashCode() + 37 * listeners.hashCode(); } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java index 6824d5e3d2..a52af88c8e 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/ConcatenatedFeatureSet.java @@ -18,14 +18,17 @@ package org.apache.sis.storage.aggregate; import java.util.Arrays; import java.util.List; +import java.util.Set; import java.util.Collection; import java.util.OptionalLong; import java.util.stream.Stream; import org.apache.sis.feature.Features; +import org.apache.sis.filter.Optimization; import org.apache.sis.storage.FeatureSet; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.DataStoreContentException; import org.apache.sis.storage.AbstractFeatureSet; +import org.apache.sis.storage.FeatureQuery; import org.apache.sis.storage.Query; import org.apache.sis.storage.Resource; import org.apache.sis.util.ArgumentChecks; @@ -70,6 +73,13 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet { */ private final FeatureType commonType; + /** + * The types, including sub-types, of all feature instances. + * This is often a singleton containing only {@link #commonType}, but it may also be a + * set without {@code commonType} if all features are instances of various subtypes. + */ + private final Set<FeatureType> allTypes; + /** * Creates a new concatenated feature set with the same types as the given feature set, * but different sources. This is used for creating {@linkplain #subset(Query) subsets}. @@ -78,6 +88,7 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet { super(original); this.sources = Containers.viewAsUnmodifiableList(sources); commonType = original.commonType; + allTypes = original.allTypes; } /** @@ -101,7 +112,8 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet { for (int i=0; i<types.length; i++) { types[i] = sources[i].getType(); } - commonType = Features.findCommonParent(Arrays.asList(types)); + allTypes = Set.copyOf(Arrays.asList(types)); + commonType = Features.findCommonParent(allTypes); if (commonType == null) { throw new DataStoreContentException(Resources.format(Resources.Keys.NoCommonFeatureType)); } @@ -240,4 +252,15 @@ public class ConcatenatedFeatureSet extends AggregatedFeatureSet { } return modified ? new ConcatenatedFeatureSet(subsets, this) : this; } + + /** + * Configures the optimization of a query with information about the expected types of all feature instances. + * This method is invoked indirectly when a {@linkplain #subset feature subset} is created from a query. + * + * @since 1.6 + */ + @Override + protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException { + optimizer.setFinalFeatureTypes(allTypes); + } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/JoinFeatureSet.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/JoinFeatureSet.java index df2843b599..9837755c4e 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/JoinFeatureSet.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/aggregate/JoinFeatureSet.java @@ -36,6 +36,7 @@ import org.apache.sis.util.ArraysExt; import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.util.collection.Containers; import org.apache.sis.filter.DefaultFilterFactory; +import org.apache.sis.filter.Optimization; // Specific to the geoapi-3.1 and geoapi-4.0 branches: import org.opengis.feature.Feature; @@ -69,7 +70,7 @@ import org.opengis.filter.BinaryComparisonOperator; * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @version 1.6 * @since 1.0 */ public class JoinFeatureSet extends AggregatedFeatureSet { @@ -314,6 +315,19 @@ public class JoinFeatureSet extends AggregatedFeatureSet { return StreamSupport.stream(it, parallel).onClose(it); } + /** + * Configures the optimization of a query with information about the expected type of all feature instances. + * The default implementation uses the knowledge that all features will be instances of exactly the type + * returned by {@link #getType()}, with no sub-type. + * Subclasses should override this method if they break this assertion. + * + * @since 1.6 + */ + @Override + protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException { + optimizer.setFinalFeatureType(getType()); + } + /** * Creates a new features containing an association to the two given features. * The {@code main} feature cannot be null (this is not verified). diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java index 3ed3d3f62c..15b79f539f 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java @@ -240,6 +240,11 @@ public class Resources extends IndexedResourceBundle { */ public static final short FeatureAlreadyPresent_2 = 16; + /** + * Feature of type “{1}” is not assignable to base type “{0}”. + */ + public static final short FeatureNotAssignableToBaseType_2 = 86; + /** * Feature “{1}” has not been found in the “{0}” data store. */ diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties index 3857d9333f..9c688036fb 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties @@ -55,6 +55,7 @@ DuplicatedQueryProperty_3 = Query property \u201c{0}\u201d is duplicated ExcessiveHeaderSize_1 = Header in the \u201c{0}\u201d file is too large. ExcessiveStringSize_3 = Character string in the \u201c{0}\u201d file is too long. The string has {2} characters while the limit is {1}. FeatureAlreadyPresent_2 = A feature named \u201c{1}\u201d is already present in the \u201c{0}\u201d data store. +FeatureNotAssignableToBaseType_2 = Feature of type \u201c{1}\u201d is not assignable to base type \u201c{0}\u201d. FeatureNotFound_2 = Feature \u201c{1}\u201d has not been found in the \u201c{0}\u201d data store. FileAlreadyExists_2 = A {1,choice,0#file|1#directory} already exists at \u201c{0}\u201d. FileIsNotAResourceDirectory_1 = The \u201c{0}\u201d file is not a directory of resources. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties index 764205bd8a..8d63ff1001 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties @@ -60,6 +60,7 @@ DuplicatedQueryProperty_3 = La propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f ExcessiveHeaderSize_1 = L\u2019en-t\u00eate dans le fichier \u00ab\u202f{0}\u202f\u00bb est trop grand. ExcessiveStringSize_3 = La cha\u00eene de caract\u00e8res dans le fichier \u00ab\u202f{0}\u202f\u00bb est trop longue. La cha\u00eene fait {2} caract\u00e8res alors que la limite est {1}. FeatureAlreadyPresent_2 = Une entit\u00e9 nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb est d\u00e9j\u00e0 pr\u00e9sente dans les donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb. +FeatureNotAssignableToBaseType_2 = L\u2019entit\u00e9 de type \u00ab\u202f{1}\u202f\u00bb n\u2019est pas assignable au type de base \u00ab\u202f{0}\u202f\u00bb. FeatureNotFound_2 = L\u2019entit\u00e9 \u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e dans les donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb. FileAlreadyExists_2 = Un {1,choice,0#fichier|1#r\u00e9pertoire} existe d\u00e9j\u00e0 \u00e0 l\u2019emplacement \u00ab\u202f{0}\u202f\u00bb. FileIsNotAResourceDirectory_1 = Le fichier \u00ab\u202f{0}\u202f\u00bb n\u2019est pas un r\u00e9pertoire de ressources. diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java index 60823765c4..1fc1bea51d 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java @@ -55,7 +55,7 @@ import org.apache.sis.pending.jdk.JDK19; * </ul> * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.5 + * @version 1.6 * @since 0.3 */ public final class Classes { @@ -355,7 +355,10 @@ public final class Classes { * @param <T> the base type of elements in the given collection. * @param objects the collection of objects. * @return the set of classes of all objects in the given collection. + * + * @deprecated To be removed after removal of public deprecated methods. */ + @Deprecated(since = "1.6", forRemoval = true) private static <T> Set<Class<? extends T>> getClasses(final Iterable<? extends T> objects) { final Set<Class<? extends T>> types = new LinkedHashSet<>(); for (final T object : objects) { @@ -548,7 +551,10 @@ next: for (final Class<?> candidate : candidates) { * @param objects a collection of objects. May contains duplicated values and null values. * @return the most specialized class, or {@code null} if the given collection does not contain * at least one non-null element. + * + * @deprecated This method is confusing as it works on instances instead of classes. */ + @Deprecated(since = "1.6", forRemoval = true) public static Class<?> findSpecializedClass(final Iterable<?> objects) { final Set<Class<?>> types = getClasses(objects); types.remove(null); @@ -600,7 +606,11 @@ next: for (final Class<?> candidate : candidates) { * @param objects a collection of objects. May contains duplicated values and null values. * @return the most specific class common to all supplied objects, or {@code null} if the * given collection does not contain at least one non-null element. + * + * @deprecated This method is confusing as it works on instances while {@link #findCommonClass(Class, Class)} + * works on classes. */ + @Deprecated(since = "1.6", forRemoval = true) public static Class<?> findCommonClass(final Iterable<?> objects) { final Set<Class<?>> types = getClasses(objects); types.remove(null); diff --git a/incubator/src/org.apache.sis.portrayal.map/test/org/apache/sis/map/SEPortrayerTest.java b/incubator/src/org.apache.sis.portrayal.map/test/org/apache/sis/map/SEPortrayerTest.java index c83d5795da..459b85eeb9 100644 --- a/incubator/src/org.apache.sis.portrayal.map/test/org/apache/sis/map/SEPortrayerTest.java +++ b/incubator/src/org.apache.sis.portrayal.map/test/org/apache/sis/map/SEPortrayerTest.java @@ -154,7 +154,7 @@ public class SEPortrayerTest { shark1.setPropertyValue("specie", "White Shark"); shark1.setPropertyValue("length", 12.0); - fishes = new MemoryFeatureSet(null, sharkType, List.of(fish1, fish2, shark1)); + fishes = new MemoryFeatureSet(null, fishType, List.of(fish1, fish2, shark1)); final FeatureTypeBuilder boatbuilder = new FeatureTypeBuilder(); boatbuilder.setName("boat"); 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 9e0efb2357..bee856c6c8 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 @@ -287,7 +287,7 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe return featureSetView.getFileSet(); } - private class AsFeatureSet extends AbstractFeatureSet implements WritableFeatureSet { + private final class AsFeatureSet extends AbstractFeatureSet implements WritableFeatureSet { private final Rectangle2D.Double filter; private final Set<String> dbfProperties; @@ -325,6 +325,16 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe return filter == null && dbfProperties == null && readShp; } + /** + * Configures the optimization of a query with the knowledge that the feature type is final. + * This configuration asserts that all features will be instances of the type returned by + * {@link #getType()} with no sub-type. + */ + @Override + protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException { + optimizer.setFinalFeatureType(getType()); + } + @Override public synchronized FeatureType getType() throws DataStoreException { if (type == null) { @@ -579,7 +589,7 @@ public final class ShapefileStore extends DataStore implements WritableFeatureSe if (selection != null) { //run optimizations final Optimization optimization = new Optimization(); - optimization.setFeatureType(type); + optimization.setFinalFeatureType(type); final ThreadLocal<Consumer<WarningEvent>> context = WarningEvent.LISTENER; final Consumer<WarningEvent> old = context.get(); try { diff --git a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java index 0bbb5e6237..e517ec3fa4 100644 --- a/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java +++ b/optional/src/org.apache.sis.storage.gdal/main/org/apache/sis/storage/gdal/FeatureLayer.java @@ -30,12 +30,14 @@ import org.apache.sis.feature.builder.AttributeRole; import org.apache.sis.feature.builder.AttributeTypeBuilder; import org.apache.sis.feature.builder.FeatureTypeBuilder; import org.apache.sis.feature.internal.shared.AttributeConvention; +import org.apache.sis.filter.Optimization; import org.apache.sis.geometry.GeneralEnvelope; import org.apache.sis.geometry.wrapper.Geometries; import org.apache.sis.geometry.wrapper.GeometryType; import org.apache.sis.referencing.IdentifiedObjects; import org.apache.sis.storage.AbstractFeatureSet; import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.FeatureQuery; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.Workaround; import org.apache.sis.util.resources.Errors; @@ -68,6 +70,7 @@ final class FeatureLayer extends AbstractFeatureSet { /** * Description (list of fields) of all feature instances in this layer. + * All features will be instances of that type, with no sub-types. */ final FeatureType type; @@ -326,6 +329,15 @@ final class FeatureLayer extends AbstractFeatureSet { } } + /** + * Configures the optimization of a query with the knowledge that the feature type is final. + * This configuration asserts that all features will be instances of {@link #type} with no sub-type. + */ + @Override + protected void prepareQueryOptimization(FeatureQuery query, Optimization optimizer) throws DataStoreException { + optimizer.setFinalFeatureType(type); + } + /** * Returns the locale-dependent resources for error messages. */
