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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new c5bd9412dd Deprecate for removal the 
`AbstractFeature.getValueOrFallback(…)` method because experience suggests that 
it encourages bugs in user's codes that stay unnoticed. That method can be 
replaced by the new `FeatureType.hasProperty(String)` method. However, that 
latter method is actually not needed often.
c5bd9412dd is described below

commit c5bd9412dd452f924c3d5f56dbe0d901e4999074
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Oct 28 19:17:38 2025 +0100

    Deprecate for removal the `AbstractFeature.getValueOrFallback(…)` method 
because
    experience suggests that it encourages bugs in user's codes that stay 
unnoticed.
    That method can be replaced by the new `FeatureType.hasProperty(String)` 
method.
    However, that latter method is actually not needed often.
    
    The most important replacement for `getValueOrFallback(…)` is to do a 
better handling
    of `PropertyNotFoundException` as warnings. When that warning occurs, this 
is usually
    because we have not done a good job in eliminating dead expressions and 
dead filters.
    So instead of invoking `hasProperty(String)` everywhere, this commit 
improves filter
    optimizations so that a `PropertyNotFoundException` would be a real error.
---
 .../org/apache/sis/feature/AbstractFeature.java    |  63 ++----------
 .../apache/sis/feature/DefaultAssociationRole.java |   5 +-
 .../org/apache/sis/feature/DefaultFeatureType.java |  19 +++-
 .../main/org/apache/sis/feature/DenseFeature.java  |   3 +
 .../org/apache/sis/feature/NamedFeatureType.java   |  13 ++-
 .../main/org/apache/sis/feature/SparseFeature.java |   3 +
 .../internal/shared/AttributeConvention.java       |  15 ---
 .../internal/shared/FeatureProjectionBuilder.java  |  11 +++
 .../sis/feature/internal/shared/FeatureView.java   |   8 +-
 .../main/org/apache/sis/feature/package-info.java  |   2 +-
 .../org/apache/sis/filter/AssociationValue.java    |   9 +-
 .../org/apache/sis/filter/BinarySpatialFilter.java |   2 +
 .../apache/sis/filter/DefaultFilterFactory.java    |   6 +-
 .../main/org/apache/sis/filter/DistanceFilter.java |   1 +
 .../org/apache/sis/filter/IdentifierFilter.java    |  16 +--
 .../main/org/apache/sis/filter/LeafExpression.java |  64 ++++++++----
 .../main/org/apache/sis/filter/Optimization.java   |   8 +-
 .../main/org/apache/sis/filter/PropertyValue.java  |  28 ++++--
 .../main/org/apache/sis/filter/internal/Node.java  |   4 +-
 .../sis/filter/internal/shared/WarningEvent.java   |  14 ++-
 .../main/org/apache/sis/filter/package-info.java   |   2 +-
 .../apache/sis/feature/AbstractFeatureTest.java    |   8 --
 .../apache/sis/filter/ArithmeticFunctionTest.java  |   3 +-
 .../apache/sis/filter/ComparisonFilterTest.java    |   3 +-
 .../apache/sis/filter/IdentifierFilterTest.java    |  58 +++++++++--
 .../org/apache/sis/filter/LeafExpressionTest.java  |   3 +-
 .../org/apache/sis/filter/LogicalFilterTest.java   |   3 +-
 .../org/apache/sis/filter/sqlmm/SQLMMTest.java     |   2 +-
 .../apache/sis/storage/sql/feature/Database.java   |  42 +++++++-
 .../sis/storage/sql/feature/FeatureStream.java     |  23 +++--
 .../apache/sis/storage/sql/feature/Resources.java  |   4 +-
 .../sis/storage/sql/feature/Resources.properties   |   2 +-
 .../storage/sql/feature/Resources_fr.properties    |   2 +-
 .../sis/storage/sql/feature/SelectionClause.java   |  47 +--------
 .../org/apache/sis/storage/sql/feature/Table.java  |   3 +-
 .../org/apache/sis/util/stream/StreamWrapper.java  | 107 +++++++++++++--------
 .../org/apache/sis/storage/sql/SQLStoreTest.java   |   2 +-
 .../sql/feature/SelectionClauseWriterTest.java     |   2 +-
 .../sis/storage/aggregate/JoinFeatureSet.java      |  13 ++-
 .../org/apache/sis/storage/FeatureQueryTest.java   |  34 +++----
 .../sis/storage/aggregate/JoinFeatureSetTest.java  |  10 +-
 geoapi/snapshot                                    |   2 +-
 .../test/org/apache/sis/map/SEPortrayerTest.java   |  23 +++++
 .../apache/sis/gui/dataset/ExpandedFeature.java    |   8 --
 44 files changed, 405 insertions(+), 295 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
index ae57b3522e..e8d683edcf 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
@@ -17,7 +17,6 @@
 package org.apache.sis.feature;
 
 import java.util.Objects;
-import java.util.Optional;
 import java.util.Iterator;
 import java.util.Collection;
 import java.util.Collections;
@@ -87,7 +86,7 @@ import org.opengis.feature.Operation;
  * @author  Travis L. Pinney
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
  *
  * @see DefaultFeatureType#newInstance()
  *
@@ -378,14 +377,10 @@ public abstract class AbstractFeature implements Feature, 
Serializable {
 
     /**
      * Returns the value for the property of the given name if that property 
exists, or a fallback value otherwise.
-     * This method is equivalent to the following code, but potentially more 
efficient when the property does not exist:
+     * This method is equivalent to the following code, but potentially more 
efficient:
      *
      * {@snippet lang="java" :
-     *     try {
-     *         return getPropertyValue(name);
-     *     } catch (PropertyNotFoundException ignore) {
-     *         return missingPropertyFallback
-     *     }
+     *     return type.hasProperty(name) ? getPropertyValue(name) : 
missingPropertyFallback;
      *     }
      *
      * Note that if a property of the given name exists but has no value, then 
this method returns the
@@ -399,9 +394,13 @@ public abstract class AbstractFeature implements Feature, 
Serializable {
      *         if no attribute or association of that name exists. This value 
may be {@code null}.
      *
      * @since 1.1
+     *
+     * @deprecated Experience suggests that this method encourage bugs in 
user's code that stay unnoticed.
      */
-    @Override
-    public abstract Object getValueOrFallback(final String name, Object 
missingPropertyFallback);
+    @Deprecated(since = "1.5", forRemoval = true)
+    public Object getValueOrFallback(final String name, Object 
missingPropertyFallback) {
+        return type.hasProperty(name) ? getPropertyValue(name) : 
missingPropertyFallback;
+    }
 
     /**
      * Executes the parameterless operation of the given name and returns the 
value of its result.
@@ -475,50 +474,6 @@ public abstract class AbstractFeature implements Feature, 
Serializable {
         }
     }
 
-    /**
-     * Returns the explicit or default value of a characteristic of a property.
-     * This is a shortcut for the following chain of method invocations
-     * (cast and null checks omitted for brevity),
-     * except that the actual implementation is potentially more efficient:
-     *
-     * {@snippet lang="java" :
-     * return Optional.ofNullable(
-     *         ((Attribute<?>) getProperty(property))
-     *         .characteristics()
-     *         .get(characteristic)
-     *         .getValue());
-     * }
-     *
-     * If the attribute has no {@linkplain AbstractAttribute#characteristics() 
characteristic} of the given name,
-     * then this method fallbacks on the default value of the {@linkplain 
DefaultAttributeType#characteristics()
-     * characteristics of the attribute type}.
-     *
-     * @param  property        name of the property for which to get a 
characteristic.
-     * @param  characteristic  name of the characteristic of the property of 
the given name.
-     * @return value of the specified characteristic on the specified 
property, or an empty value
-     *         if the property is not an attribute or the attribute has no 
such characteristic.
-     * @throws PropertyNotFoundException if the {@code property} argument is 
not the name of a property of this feature.
-     *
-     * @since 1.5
-     */
-    public Optional<?> getCharacteristicValue(final String property, final 
String characteristic)
-            throws PropertyNotFoundException
-    {
-        Property p = getProperty(property);
-        if (p instanceof Attribute<?>) {
-            var attribute = (Attribute<?>) p;
-            Attribute<?> ca = attribute.characteristics().get(characteristic);
-            if (ca != null) {
-                // If the characteristic is present, assume that an explicitly 
null value is intentional.
-                return Optional.ofNullable(ca.getValue());
-            } else {
-                return 
Optional.ofNullable(attribute.getType().characteristics().get(characteristic))
-                        .map(AttributeType::getDefaultValue);
-            }
-        }
-        return Optional.empty();
-    }
-
     /**
      * Returns the value of the given attribute, as a singleton or as a 
collection depending
      * on the maximum number of occurrences.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
index aba59be67e..e479c9bb4a 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultAssociationRole.java
@@ -34,7 +34,6 @@ import org.opengis.feature.AttributeType;
 import org.opengis.feature.FeatureType;
 import org.opengis.feature.FeatureAssociation;
 import org.opengis.feature.FeatureAssociationRole;
-import org.opengis.feature.PropertyNotFoundException;
 
 
 /**
@@ -414,10 +413,8 @@ public class DefaultAssociationRole extends FieldType 
implements FeatureAssociat
      */
     private static String searchTitleProperty(final FeatureType ft) {
         String fallback = null;
-        try {
+        if (ft.hasProperty(AttributeConvention.IDENTIFIER)) {
             return 
ft.getProperty(AttributeConvention.IDENTIFIER).getName().toString();
-        } catch (PropertyNotFoundException e) {
-            // Ignore.
         }
         for (final PropertyType type : ft.getProperties(true)) {
             if (type instanceof AttributeType<?>) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
index fb9a800298..35fe577163 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DefaultFeatureType.java
@@ -97,7 +97,7 @@ import org.opengis.feature.PropertyNotFoundException;
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.6
  *
  * @see DefaultAttributeType
  * @see DefaultAssociationRole
@@ -850,8 +850,25 @@ public class DefaultFeatureType extends 
AbstractIdentifiedType implements Featur
         return includeSuperTypes ? allProperties : properties;
     }
 
+    /**
+     * Returns {@code true} if and only if an attribute, operation or 
association role of the given name exists
+     * in this feature type or in one of its super-types. If this method 
returns {@code true}, then calls to
+     * <code>{@linkplain #getProperty(String) getProperty}(name)</code> will 
not throw
+     * {@link PropertyNotFoundException}.
+     *
+     * @param  name  the name of the property to search.
+     * @return whether an attribute, operation or association role exists for 
the given name.
+     *
+     * @since 1.6
+     */
+    @Override
+    public boolean hasProperty(final String name) {
+        return byName.get(name) != null;
+    }
+
     /**
      * Returns the attribute, operation or association role for the given name.
+     * The method searches in this feature type and in all super-types.
      *
      * @param  name  the name of the property to search.
      * @return the property for the given name, or {@code null} if none.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DenseFeature.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DenseFeature.java
index af8344ec28..38315596a9 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DenseFeature.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/DenseFeature.java
@@ -178,8 +178,11 @@ final class DenseFeature extends AbstractFeature 
implements CloneAccess {
      * @param  name  the property name.
      * @param  missingPropertyFallback  the value to return if no attribute or 
association of the given name exists.
      * @return the value for the given property, or {@code null} if none.
+     *
+     * @deprecated Experience suggests that this method encourage bugs in 
user's code that stay unnoticed.
      */
     @Override
+    @Deprecated(since = "1.5", forRemoval = true)
     public final Object getValueOrFallback(final String name, final Object 
missingPropertyFallback) {
         ArgumentChecks.ensureNonNull("name", name);
         final Integer index = indices.get(name);
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/NamedFeatureType.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/NamedFeatureType.java
index c1517e71f9..1b07cbd5a9 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/NamedFeatureType.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/NamedFeatureType.java
@@ -101,11 +101,19 @@ final class NamedFeatureType implements FeatureType, 
Serializable {
         return false;
     }
 
+    /**
+     * Always returns {@code false} since this feature type has no declared 
property yet.
+     */
+    @Override
+    public boolean hasProperty(String name) {
+        return false;
+    }
+
     /**
      * Always throws {@link PropertyNotFoundException} since this feature type 
has no declared property yet.
      */
     @Override
-    public PropertyType getProperty(final String name) throws 
PropertyNotFoundException {
+    public PropertyType getProperty(String name) throws 
PropertyNotFoundException {
         throw new 
PropertyNotFoundException(Resources.format(Resources.Keys.PropertyNotFound_2, 
getName(), name));
     }
 
@@ -113,7 +121,7 @@ final class NamedFeatureType implements FeatureType, 
Serializable {
      * Returns an empty set since this feature has no declared property yet.
      */
     @Override
-    public Collection<? extends PropertyType> getProperties(final boolean 
includeSuperTypes) {
+    public Collection<? extends PropertyType> getProperties(boolean 
includeSuperTypes) {
         return Collections.emptySet();
     }
 
@@ -139,6 +147,7 @@ final class NamedFeatureType implements FeatureType, 
Serializable {
         if (type == null) {
             return false;
         }
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final FeatureType resolved = this.resolved;
         return (resolved != null) && resolved.isAssignableFrom(type);
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/SparseFeature.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/SparseFeature.java
index 6ddc2667dd..f49577495f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/SparseFeature.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/SparseFeature.java
@@ -225,8 +225,11 @@ final class SparseFeature extends AbstractFeature 
implements CloneAccess {
      * @param  name  the property name.
      * @param  missingPropertyFallback  the value to return if no attribute or 
association of the given name exists.
      * @return the value for the given property, or {@code null} if none.
+     *
+     * @deprecated Experience suggests that this method encourage bugs in 
user's code that stay unnoticed.
      */
     @Override
+    @Deprecated(since = "1.5", forRemoval = true)
     public final Object getValueOrFallback(final String name, final Object 
missingPropertyFallback) {
         ArgumentChecks.ensureNonNull("name", name);
         final Integer index = indices.get(name);
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/AttributeConvention.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/AttributeConvention.java
index 70e3c6b2ef..395db909dc 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/AttributeConvention.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/AttributeConvention.java
@@ -249,21 +249,6 @@ public final class AttributeConvention {
         return false;
     }
 
-    /**
-     * Returns {@code true} if the given feature type is non-null and has a 
{@value #IDENTIFIER} property.
-     *
-     * @param  feature  the feature type to test, or {@code null}.
-     * @return whether the given feature type is non-null and has a {@value 
#IDENTIFIER} property.
-     */
-    public static boolean hasIdentifier(final FeatureType feature) {
-        if (feature != null) try {
-            return feature.getProperty(IDENTIFIER) != null;
-        } catch (PropertyNotFoundException e) {
-            // Ignore
-        }
-        return false;
-    }
-
     /**
      * Returns {@code true} if the given type is an {@link AttributeType} or 
an {@link Operation} computing
      * an attribute, and the attribute value is one of the geometry types 
recognized by SIS.
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 ac9681ef89..c1e57566b3 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
@@ -36,6 +36,7 @@ import org.apache.sis.feature.builder.AttributeTypeBuilder;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.feature.builder.PropertyTypeBuilder;
 import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.filter.Optimization;
 import org.apache.sis.util.ArgumentCheckByAssertion;
 import org.apache.sis.util.UnconvertibleObjectException;
 import org.apache.sis.util.internal.shared.Strings;
@@ -551,6 +552,13 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
             return attributeValueGetter;
         }
 
+        /**
+         * Optimizes the expression. This is invoked as the last step before 
to build the final feature projection.
+         */
+        final void optimize(final Optimization optimizer) {
+            attributeValueGetter = optimizer.apply(attributeValueGetter);
+        }
+
         /**
          * Sets the coordinate reference system that characterizes the values 
of this attribute.
          *
@@ -766,6 +774,9 @@ public final class FeatureProjectionBuilder extends 
FeatureTypeBuilder {
         if (source.equals(typeRequested) && 
source.equals(typeWithDependencies)) {
             return Optional.empty();
         }
+        final var optimizer = new Optimization();
+        optimizer.setFeatureType(source);
+        requested.forEach((item) -> item.optimize(optimizer));
         return Optional.of(new FeatureProjection(typeRequested, 
typeWithDependencies, requested));
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureView.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureView.java
index 077d491398..931748153f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureView.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/shared/FeatureView.java
@@ -123,9 +123,15 @@ final class FeatureView extends AbstractFeature {
      * @param  name  the property name.
      * @param  missingPropertyFallback  the value to return if no attribute or 
association of the given name exists.
      * @return value or default value of the specified property, or {@code 
missingPropertyFallback}.
+     *
+     * @deprecated Experience suggests that this method encourage bugs in 
user's code that stay unnoticed.
      */
     @Override
+    @Deprecated(since = "1.5", forRemoval = true)
     public Object getValueOrFallback(final String name, final Object 
missingPropertyFallback) {
-        return source.getValueOrFallback(name, missingPropertyFallback);
+        if (source instanceof AbstractFeature) {
+            return ((AbstractFeature) source).getValueOrFallback(name, 
missingPropertyFallback);
+        }
+        return super.getValueOrFallback(name, missingPropertyFallback);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/package-info.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/package-info.java
index b7fe070c00..749cea6798 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/package-info.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/package-info.java
@@ -93,7 +93,7 @@
  * @author  Travis L. Pinney
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
  * @since   0.5
  */
 package org.apache.sis.feature;
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 657a64b49f..2e0f126734 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
@@ -171,7 +171,7 @@ walk:   if (specifiedType != null) try {
              * Delegate the final property optimization to `accessor` which 
may not only resolve
              * links but also tune the `ObjectConverter`.
              */
-            final PropertyValue<V> converted;
+            final Expression<Feature, V> converted;
             optimization.setFeatureType(type);
             try {
                 converted = accessor.optimize(optimization);
@@ -179,7 +179,12 @@ walk:   if (specifiedType != null) try {
                 optimization.setFeatureType(specifiedType);
             }
             if (converted != accessor || direct != path) {
-                return new AssociationValue<>(direct, converted);
+                if (converted instanceof PropertyValue<?>) {
+                    return new AssociationValue<>(direct, (PropertyValue<V>) 
converted);
+                } else {
+                    // If not a `PropertyValue`, then it should be a `Literal`.
+                    return converted;
+                }
             }
         } catch (PropertyNotFoundException e) {
             warning(e, true);
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinarySpatialFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinarySpatialFilter.java
index 3e03ba7213..bda0bd58fe 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinarySpatialFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinarySpatialFilter.java
@@ -64,6 +64,7 @@ final class BinarySpatialFilter<R> extends 
BinaryGeometryFilter<R> implements Bi
     BinarySpatialFilter(final Geometries<?> library, final Expression<R,?> 
geometry,
                         final Envelope bounds, final WraparoundMethod 
wraparound)
     {
+        // Checks for null value are done indirectly in the methods invoked 
below.
         super(library, geometry, new 
LeafExpression.Transformed<>(library.toGeometry2D(bounds, wraparound),
                                  new LeafExpression.Literal<>(bounds)), null);
         operatorType = SpatialOperatorName.BBOX;
@@ -138,6 +139,7 @@ final class BinarySpatialFilter<R> extends 
BinaryGeometryFilter<R> implements Bi
      * @return {@code true} if the test(s) are passed for the provided object.
      */
     @Override
+    @SuppressWarnings("UseSpecificCatch")
     public boolean test(final R object) {
         final GeometryWrapper left = expression1.apply(object);
         if (left != null) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
index 73d2ed0d0d..22bab587df 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DefaultFilterFactory.java
@@ -189,8 +189,7 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
          *
          * @see #forFeatures()
          */
-        static final FilterFactory<Feature,Object,Object> DEFAULT =
-                new Features<>(Object.class, Object.class, 
WraparoundMethod.SPLIT);
+        static final Features<Object,Object> DEFAULT = new 
Features<>(Object.class, Object.class, WraparoundMethod.SPLIT);
 
         /**
          * Creates a new factory operating on {@link Feature} instances.
@@ -284,6 +283,9 @@ public abstract class DefaultFilterFactory<R,G,T> extends 
AbstractFactory implem
      */
     @Override
     public <V> Literal<R,V> literal(final V value) {
+        if (value == null) {
+            return LeafExpression.NULL();
+        }
         return new LeafExpression.Literal<>(value);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DistanceFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DistanceFilter.java
index 3fd8a10447..c71d0c641e 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DistanceFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DistanceFilter.java
@@ -155,6 +155,7 @@ final class DistanceFilter<R> extends 
BinaryGeometryFilter<R> implements Distanc
      * @return {@code true} if the test(s) are passed for the provided object.
      */
     @Override
+    @SuppressWarnings("UseSpecificCatch")
     public boolean test(final R object) {
         final GeometryWrapper left = expression1.apply(object);
         if (left != null) {
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 f2a7b4ef71..39d8bc7b3d 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
@@ -18,12 +18,13 @@ package org.apache.sis.filter;
 
 import java.util.List;
 import java.util.Collection;
-import org.apache.sis.util.ArgumentChecks;
+import java.util.Objects;
 import org.apache.sis.filter.internal.Node;
 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.PropertyNotFoundException;
 import org.opengis.filter.Expression;
 import org.opengis.filter.ResourceId;
 import org.opengis.filter.Filter;
@@ -51,8 +52,7 @@ final class IdentifierFilter extends Node implements 
ResourceId<Feature>, Optimi
      * Creates a new filter using the given identifier.
      */
     IdentifierFilter(final String identifier) {
-        ArgumentChecks.ensureNonEmpty("identifier", identifier);
-        this.identifier = identifier;
+        this.identifier = Objects.requireNonNull(identifier);
     }
 
     /**
@@ -103,10 +103,12 @@ final class IdentifierFilter extends Node implements 
ResourceId<Feature>, Optimi
      */
     @Override
     public boolean test(final Feature object) {
-        if (object == null) {
-            return false;
+        if (object != null) try {
+            Object id = 
object.getPropertyValue(AttributeConvention.IDENTIFIER);
+            if (id != null) return identifier.equals(id.toString());
+        } catch (PropertyNotFoundException e) {
+            warning(e, false);
         }
-        final Object id = 
object.getValueOrFallback(AttributeConvention.IDENTIFIER, null);
-        return (id != null) && identifier.equals(id.toString());
+        return false;
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
index 77d3b85e97..8008b303e5 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/LeafExpression.java
@@ -77,6 +77,18 @@ abstract class LeafExpression<R,V> extends Node implements 
FeatureExpression<R,V
         return Set.of();
     }
 
+    /**
+     * Returns a literal which always returns {@code null}.
+     *
+     * @param  <R>  ignored.
+     * @param  <V>  ignored.
+     * @return a literal for {@code null}.
+     */
+    @SuppressWarnings("unchecked")
+    protected static <R,V> Literal<R,V> NULL() {
+        return (Literal) Literal.NULL;
+    }
+
 
 
 
@@ -95,6 +107,9 @@ abstract class LeafExpression<R,V> extends Node implements 
FeatureExpression<R,V
         /** For cross-version compatibility. */
         private static final long serialVersionUID = -8383113218490957822L;
 
+        /** A predefined literal which always returns {@code null}. */
+        private static Literal<?,?> NULL = new Literal<>(null);
+
         /** The constant value to be returned by {@link #getValue()}. */
         @SuppressWarnings("serial")         // Not statically typed as 
Serializable.
         protected final V value;
@@ -138,16 +153,32 @@ abstract class LeafExpression<R,V> extends Node 
implements FeatureExpression<R,V
          */
         @Override
         @SuppressWarnings("unchecked")
-        public <N> Expression<R,N> toValueType(final Class<N> target) {
+        public final <N> Expression<R,N> toValueType(final Class<N> target) {
             try {
                 final N c = ObjectConverters.convert(value, target);
-                return (c != value) ? new Literal<>(c) : (Literal<R,N>) this;
+                if (c == null)  return (Literal<R,N>) NULL;
+                if (c == value) return (Literal<R,N>) this;
+                return new Literal<>(c);
             } catch (UnconvertibleObjectException e) {
-                throw (ClassCastException) new 
ClassCastException(Errors.format(
-                        Errors.Keys.CanNotConvertValue_2, getFunctionName(), 
target)).initCause(e);
+                return unconvertibleValue(target, e);
             }
         }
 
+        /**
+         * Invoked when {@link #toValueType(Class)} failed to find a converter 
to the given class.
+         * This method gives a chance to provide a fallback.
+         *
+         * @param  <N>     compile-time value of {@code target}.
+         * @param  target  the target class requested by the user.
+         * @param  cause   the exception that occurred.
+         * @return the fallback.
+         * @throws ClassCastException if there is no fallback.
+         */
+        protected <N> Expression<R,N> unconvertibleValue(final Class<N> 
target, UnconvertibleObjectException cause) {
+            throw (ClassCastException) new ClassCastException(Errors.format(
+                    Errors.Keys.CanNotConvertValue_2, getFunctionName(), 
target)).initCause(cause);
+        }
+
         /**
          * Provides the type of values returned by {@link #apply(Object)}.
          * The returned item wraps an {@link AttributeType} named "Literal".
@@ -227,26 +258,19 @@ abstract class LeafExpression<R,V> extends Node 
implements FeatureExpression<R,V
         }
 
         /**
-         * Converts the transformed value if possible, or the original value 
as a fallback.
-         *
-         * @throws ClassCastException if values cannot be provided as 
instances of the specified class.
+         * Invoked when {@link #toValueType(Class)} failed to find a converter 
to the given class.
+         * This method tries to apply the same operation on the original value 
as a fallback.
          */
         @Override
-        @SuppressWarnings("unchecked")
-        public <N> Expression<R,N> toValueType(final Class<N> target) {
-            // Same implementation as `super.toValueType(type)` except for 
exception handling.
+        protected <N> Expression<R,N> unconvertibleValue(final Class<N> 
target, UnconvertibleObjectException cause) {
             try {
-                final N c = ObjectConverters.convert(value, target);
-                return (c != value) ? new Literal<>(c) : (Literal<R,N>) this;
-            } catch (UnconvertibleObjectException e) {
+                return original.toValueType(target);
+            } catch (RuntimeException bis) {
                 try {
-                    return original.toValueType(target);
-                } catch (RuntimeException bis) {
-                    final var c = new ClassCastException(Errors.format(
-                            Errors.Keys.CanNotConvertValue_2, 
getFunctionName(), target));
-                    c.initCause(e);
-                    c.addSuppressed(bis);
-                    throw c;
+                    return super.unconvertibleValue(target, cause);     // For 
creating the main exception.
+                } catch (RuntimeException e) {
+                    e.addSuppressed(bis);
+                    throw e;
                 }
             }
         }
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 71c84d6fe1..1c0cb4bc3b 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
@@ -45,6 +45,7 @@ import org.opengis.feature.FeatureType;
  * <ul>
  *   <li>Application of some logical identities such as {@code NOT(NOT(A)) == 
A}.</li>
  *   <li>Application of logical short circuits such as {@code A & FALSE == 
FALSE}.</li>
+ *   <li>Replacement of value references to non-existent properties by null 
literals.</li>
  *   <li>Immediate evaluation of expressions where all parameters are literal 
values.</li>
  * </ul>
  *
@@ -72,13 +73,13 @@ import org.opengis.feature.FeatureType;
  * <h2>Behavioral changes</h2>
  * Optimized filters shall produce the same results as non-optimized filters.
  * However side-effects may differ, in particular regarding exceptions that 
may be thrown.
- * For example if a filter tests {@code A & B} and if {@code Optimization} 
determines that the {@code B}
+ * For example, if a filter tests {@code A & B} and if {@code Optimization} 
determines that the {@code B}
  * condition will always evaluate to {@code false}, then the {@code A} 
condition will never be tested.
  * If that condition had side-effects or threw an exception,
  * those effects will disappear in the optimized filter.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.6
  * @since   1.1
  */
 public class Optimization {
@@ -539,6 +540,9 @@ public class Optimization {
      * @see DefaultFilterFactory#literal(Object)
      */
     public static <R,V> Literal<R,V> literal(final V value) {
+        if (value == null) {
+            return LeafExpression.NULL();
+        }
         return new LeafExpression.Literal<>(value);
     }
 }
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 7b7d9c4d27..8df2b62ed6 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
@@ -33,6 +33,7 @@ import org.opengis.feature.FeatureType;
 import org.opengis.feature.PropertyType;
 import org.opengis.feature.AttributeType;
 import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.filter.Expression;
 import org.opengis.filter.ValueReference;
 
 
@@ -205,7 +206,7 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
      * using or not the database index.
      */
     @Override
-    public abstract PropertyValue<V> optimize(Optimization optimization);
+    public abstract Expression<Feature, V> optimize(Optimization optimization);
 
 
 
@@ -230,23 +231,29 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
          */
         @Override
         public Object apply(final Feature instance) {
-            return (instance != null) ? instance.getValueOrFallback(name, 
null) : null;
+            if (instance != null) try {
+                return instance.getPropertyValue(name);
+            } catch (PropertyNotFoundException e) {
+                warning(e, false);
+            }
+            return null;
         }
 
         /**
          * If the evaluated property is a link, replaces this expression by a 
more direct reference
          * to the target property. This optimization is important for allowing 
{@code SQLStore} to
-         * put the column name in the SQL {@code WHERE} clause. It makes the 
difference between
-         * using or not the database index.
+         * put the column name in the <abbr>SQL</abbr> {@code WHERE} clause.
+         * It makes the difference between using or not the database index.
          */
         @Override
-        public PropertyValue<Object> optimize(final Optimization optimization) 
{
+        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);
             } catch (PropertyNotFoundException e) {
                 warning(e, true);
+                return NULL();
             }
             return this;
         }
@@ -294,8 +301,8 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
         @Override
         public V apply(final Feature instance) {
             if (instance != null) try {
-                return 
ObjectConverters.convert(instance.getValueOrFallback(name, null), type);
-            } catch (UnconvertibleObjectException e) {
+                return 
ObjectConverters.convert(instance.getPropertyValue(name), type);
+            } catch (PropertyNotFoundException | UnconvertibleObjectException 
e) {
                 warning(e, false);
             }
             return null;
@@ -307,7 +314,7 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
          * then a specialized expression is returned. Otherwise this method 
returns {@code this}.
          */
         @Override
-        public final PropertyValue<V> optimize(final Optimization 
optimization) {
+        public final Expression<Feature, V> optimize(final Optimization 
optimization) {
             final FeatureType featureType = optimization.getFeatureType();
             if (featureType != null) try {
                 /*
@@ -344,6 +351,7 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
                 }
             } catch (PropertyNotFoundException e) {
                 warning(e, true);
+                return NULL();
             }
             return this;
         }
@@ -437,8 +445,8 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
         @Override
         public V apply(final Feature instance) {
             if (instance != null) try {
-                return 
converter.apply(source.cast(instance.getValueOrFallback(name, null)));
-            } catch (ClassCastException | UnconvertibleObjectException e) {
+                return 
converter.apply(source.cast(instance.getPropertyValue(name)));
+            } catch (PropertyNotFoundException | ClassCastException | 
UnconvertibleObjectException e) {
                 warning(e, false);
             }
             return null;
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
index e3842427c0..37951b1e6f 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/Node.java
@@ -385,7 +385,7 @@ public abstract class Node implements Serializable {
      *
      * @param  exception    the exception that occurred.
      * @param  recoverable  {@code true} if the caller has been able to 
fallback on a default value,
-     *                      or {@code false} if the caller has to return 
{@code null}.
+     *                      or {@code false} if the caller has to return 
{@code null} or {@code false}.
      *
      * @todo Consider defining a {@code Context} class providing, among other 
information, listeners where to report warnings.
      *
@@ -394,7 +394,7 @@ public abstract class Node implements Serializable {
     protected final void warning(final Exception exception, final boolean 
recoverable) {
         final Consumer<WarningEvent> listener = WarningEvent.LISTENER.get();
         if (listener != null) {
-            listener.accept(new WarningEvent(this, exception));
+            listener.accept(new WarningEvent(this, exception, recoverable));
         } else {
             final String method = (this instanceof Predicate) ? "test" : 
"apply";
             if (recoverable) {
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/WarningEvent.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/WarningEvent.java
index 72b098dc57..bea30abf46 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/WarningEvent.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/internal/shared/WarningEvent.java
@@ -52,15 +52,23 @@ public final class WarningEvent {
      */
     public final Exception exception;
 
+    /**
+     * {@code true} if the caller has been able to fallback on a default value,
+     * or {@code false} if the caller has to return {@code null} or {@code 
false}.
+     * If {@code true}, the warning should be logged at a finer level.
+     */
+    public final boolean recoverable;
+
     /**
      * Creates a new warning.
      *
      * @param  source     the filter or expression that produced this warning.
      * @param  exception  the exception that occurred.
      */
-    public WarningEvent(final Node source, final Exception exception) {
-        this.source    = source;
-        this.exception = exception;
+    public WarningEvent(final Node source, final Exception exception, final 
boolean recoverable) {
+        this.source      = source;
+        this.exception   = exception;
+        this.recoverable = recoverable;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/package-info.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/package-info.java
index d944af325a..52c562e1c3 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/package-info.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/package-info.java
@@ -57,7 +57,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.6
  *
  * @since 1.1
  */
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/AbstractFeatureTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/AbstractFeatureTest.java
index 637b6a10d9..3c319962a3 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/AbstractFeatureTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/feature/AbstractFeatureTest.java
@@ -100,14 +100,6 @@ public final class AbstractFeatureTest extends 
FeatureTestCase {
             return values.get(name);
         }
 
-        /**
-         * Synonymous of {@link #getPropertyValue(String)} for this test.
-         */
-        @Override
-        public Object getValueOrFallback(final String name, final Object 
missingPropertyFallback) {
-            return getPropertyValue(name);
-        }
-
         /**
          * Sets the value for the property of the given name. In order to 
allow the tests to pass,
          * we need to reproduce in this method some of the verifications 
performed by the
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ArithmeticFunctionTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ArithmeticFunctionTest.java
index 71ad083135..5d09d33270 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ArithmeticFunctionTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ArithmeticFunctionTest.java
@@ -32,11 +32,12 @@ import org.opengis.filter.FilterFactory;
  *
  * @author  Johann Sorel (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class ArithmeticFunctionTest extends TestCase {
     /**
      * The factory to use for creating the objects to test.
      */
-    private final FilterFactory<Feature,Object,?> factory;
+    private final FilterFactory<Feature, ?, ?> factory;
 
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ComparisonFilterTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ComparisonFilterTest.java
index 4385cb4629..fd3453ce0b 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ComparisonFilterTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/ComparisonFilterTest.java
@@ -39,11 +39,12 @@ import org.apache.sis.filter.internal.shared.FunctionNames;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class ComparisonFilterTest extends TestCase {
     /**
      * The factory to use for creating the objects to test.
      */
-    private final FilterFactory<Feature,Object,?> factory;
+    private final FilterFactory<Feature, ?, ?> factory;
 
     /**
      * Expressions used as constant for the tests.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/IdentifierFilterTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/IdentifierFilterTest.java
index 361e3cd26c..225e2373d8 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/IdentifierFilterTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/IdentifierFilterTest.java
@@ -16,18 +16,24 @@
  */
 package org.apache.sis.filter;
 
+import java.util.function.Consumer;
 import org.apache.sis.feature.builder.AttributeRole;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.filter.internal.shared.WarningEvent;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
 import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.test.TestCase;
+import static org.apache.sis.test.Assertions.assertMessageContains;
 import static org.apache.sis.test.Assertions.assertSerializedEquals;
 
 // 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.Filter;
 import org.opengis.filter.FilterFactory;
 
@@ -38,11 +44,17 @@ import org.opengis.filter.FilterFactory;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-public final class IdentifierFilterTest extends TestCase {
+@SuppressWarnings("exports")
+public final class IdentifierFilterTest extends TestCase implements 
Consumer<WarningEvent> {
     /**
      * The factory to use for creating the objects to test.
      */
-    private final FilterFactory<Feature,Object,?> factory;
+    private final FilterFactory<Feature, ?, ?> factory;
+
+    /**
+     * The warning that occurred while executing a filter or expression.
+     */
+    private WarningEvent warning;
 
     /**
      * Creates a new test case.
@@ -51,12 +63,40 @@ public final class IdentifierFilterTest extends TestCase {
         factory = DefaultFilterFactory.forFeatures();
     }
 
+    /**
+     * Setup a listener for warnings that may occur during expression or 
filter execution.
+     */
+    @BeforeEach
+    public void registerWarningListener() {
+        WarningEvent.LISTENER.set(this);
+    }
+
+    /**
+     * Removes the listener.
+     */
+    @AfterEach
+    public void unregisterWarningListener() {
+        WarningEvent.LISTENER.remove();
+    }
+
+    /**
+     * Invoked when a warning occurred. We expect at most one warning per test.
+     *
+     * @param  event  the warning that occurred.
+     */
+    @Override
+    public void accept(final WarningEvent event) {
+        assertNull(warning);
+        warning = event;
+    }
+
     /**
      * Tests construction and serialization.
      */
     @Test
     public void testSerialize() {
         assertSerializedEquals(factory.resourceId("abc"));
+        assertNull(warning);
     }
 
     /**
@@ -82,9 +122,11 @@ public final class IdentifierFilterTest extends TestCase {
 
         final Filter<Feature> id = factory.resourceId("123");
         assertEquals(Feature.class, id.getResourceClass());
-        assertTrue (id.test(f1));
-        assertTrue (id.test(f2));
-        assertFalse(id.test(f3));
+        assertTrue (id.test(f1)); assertNull(warning);
+        assertTrue (id.test(f2)); assertNull(warning);
+        assertFalse(id.test(f3)); assertNotNull(warning);
+        var e = assertInstanceOf(PropertyNotFoundException.class, 
warning.exception);
+        assertMessageContains(e, "sis:identifier", "Test 3");
     }
 
     /**
@@ -105,8 +147,8 @@ public final class IdentifierFilterTest extends TestCase {
                 factory.resourceId("123"));
 
         assertEquals(Feature.class, id.getResourceClass());
-        assertTrue (id.test(f1));
-        assertTrue (id.test(f2));
-        assertFalse(id.test(f3));
+        assertTrue (id.test(f1)); assertNull(warning);
+        assertTrue (id.test(f2)); assertNull(warning);
+        assertFalse(id.test(f3)); assertNull(warning);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LeafExpressionTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LeafExpressionTest.java
index cd285db672..9c63b284b5 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LeafExpressionTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/LeafExpressionTest.java
@@ -34,11 +34,12 @@ import org.opengis.filter.FilterFactory;
  *
  * @author  Johann Sorel (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class LeafExpressionTest extends TestCase {
     /**
      * The factory to use for creating the objects to test.
      */
-    private final FilterFactory<Feature,Object,?> factory;
+    private final FilterFactory<Feature, ?, ?> factory;
 
     /**
      * Creates a new test case.
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 2dcdb65554..39c3c62c35 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
@@ -43,11 +43,12 @@ import org.opengis.filter.LogicalOperator;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class LogicalFilterTest extends TestCase {
     /**
      * The factory to use for creating the objects to test.
      */
-    private final FilterFactory<Feature,Object,?> factory;
+    private final FilterFactory<Feature, ?, ?> factory;
 
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/SQLMMTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/SQLMMTest.java
index 7bd32b6664..74a758d6b3 100644
--- 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/SQLMMTest.java
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/sqlmm/SQLMMTest.java
@@ -48,7 +48,7 @@ public final class SQLMMTest extends TestCase {
     /**
      * The factory to use for creating the objects to test.
      */
-    private final FilterFactory<Feature,Object,?> factory;
+    private final FilterFactory<Feature, ?, ?> factory;
 
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java
index 3945e2e6c6..05c14066e5 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Database.java
@@ -28,6 +28,7 @@ import java.util.Optional;
 import java.util.Collections;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import java.util.function.Consumer;
 import java.util.concurrent.locks.ReadWriteLock;
 import java.sql.Array;
 import java.sql.Connection;
@@ -50,13 +51,14 @@ import org.apache.sis.storage.FeatureSet;
 import org.apache.sis.storage.FeatureNaming;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.IllegalNameException;
+import org.apache.sis.storage.sql.SQLStore;
+import org.apache.sis.storage.sql.ResourceDefinition;
 import org.apache.sis.storage.base.MetadataBuilder;
+import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.filter.internal.shared.WarningEvent;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryType;
 import org.apache.sis.system.Modules;
-import org.apache.sis.storage.sql.SQLStore;
-import org.apache.sis.storage.sql.ResourceDefinition;
-import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.util.Debug;
 import org.apache.sis.util.Version;
 import org.apache.sis.util.collection.TreeTable;
@@ -64,6 +66,8 @@ import org.apache.sis.util.collection.Cache;
 import org.apache.sis.util.internal.shared.Strings;
 import org.apache.sis.util.internal.shared.UnmodifiableArrayList;
 import org.apache.sis.util.resources.Vocabulary;
+import org.opengis.filter.ValueReference;
+import org.opengis.util.CodeList;
 
 
 /**
@@ -102,7 +106,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Alexis Manin (Geomatys)
  */
-public class Database<G> extends Syntax  {
+public class Database<G> extends Syntax {
     /**
      * The SQL wildcard for any characters. A string containing only this 
wildcard
      * means "any value" and can sometimes be replaced by {@code null}.
@@ -883,6 +887,13 @@ public class Database<G> extends Syntax  {
         return new InfoStatements(this, connection);
     }
 
+    /**
+     * Returns the localized resources for warnings and error messages.
+     */
+    private Resources resources() {
+        return Resources.forLocale(listeners.getLocale());
+    }
+
     /**
      * Logs a warning with a localized message and an optional cause.
      *
@@ -890,7 +901,7 @@ public class Database<G> extends Syntax  {
      * @param cause        the cause, or {@code null} if none.
      */
     final void warning(final short resourceKey, final Exception cause) {
-        LogRecord record = 
Resources.forLocale(listeners.getLocale()).createLogRecord(Level.WARNING, 
resourceKey);
+        LogRecord record = resources().createLogRecord(Level.WARNING, 
resourceKey);
         record.setThrown(cause);
         log(record);
     }
@@ -908,6 +919,27 @@ public class Database<G> extends Syntax  {
         listeners.warning(record);
     }
 
+    /**
+     * Creates a listener for warnings that occur during the execution of 
filters or expressions.
+     * This method declares {@link FeatureSet#features(boolean)} as the public 
source of the log.
+     *
+     * @return the warning listener.
+     */
+    final Consumer<WarningEvent> createFilterListener() {
+        return (event) -> {
+            final LogRecord record = resources().createLogRecord(
+                    event.recoverable ? Level.FINE : Level.WARNING,
+                    Resources.Keys.IncompatibleFunction_2,
+                    
event.getOperatorType().flatMap(CodeList::identifier).orElse("?"),
+                    
event.getParameter(ValueReference.class).map(ValueReference<?,?>::getXPath).orElse("?"));
+            record.setThrown(event.exception);
+            record.setSourceClassName(FeatureSet.class.getName());
+            record.setSourceMethodName("features");
+            record.setLoggerName(Modules.SQL);
+            listeners.warning(record);
+        };
+    }
+
     /**
      * Creates a tree representation of this database for debugging purpose.
      *
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 e4b9294b77..9605dd231a 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
@@ -32,7 +32,6 @@ import java.sql.SQLException;
 import java.sql.Statement;
 import org.apache.sis.filter.Optimization;
 import org.apache.sis.filter.internal.shared.SortByComparator;
-import org.apache.sis.filter.internal.shared.WarningEvent;
 import org.apache.sis.metadata.sql.internal.shared.SQLBuilder;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.internal.shared.Strings;
@@ -136,6 +135,7 @@ final class FeatureStream extends DeferredStream<Feature> {
      */
     FeatureStream(final Table table, final boolean parallel) {
         super(FeatureIterator.CHARACTERISTICS, parallel);
+        listener = table.database.createFilterListener();
         this.table = table;
     }
 
@@ -191,9 +191,8 @@ final class FeatureStream extends DeferredStream<Feature> {
          * if we have a "F₀ AND F₁ AND F₂" chain, it is possible to have some 
Fₙ as SQL statements and
          * other Fₙ executed in Java code.
          */
-        Stream<Feature> stream = this;
-        try {
-            WarningEvent.LISTENER.set(selection);
+        return execute(() -> {
+            Stream<Feature> stream = this;
             final var optimization = new Optimization();
             optimization.setFeatureType(table.featureType);
             for (final var filter : optimization.applyAndDecompose((Filter<? 
super Feature>) predicate)) {
@@ -201,14 +200,16 @@ final class FeatureStream extends DeferredStream<Feature> 
{
                 if (filter == Filter.exclude()) return empty();
                 if (!selection.tryAppend(filterToSQL, filter)) {
                     // Delegate to Java code all filters that we cannot 
translate to SQL statement.
-                    stream = super.filter(filter);
+                    if (stream == this) {
+                        stream = super.filter(filter);
+                    } else {
+                        stream = stream.filter(filter);
+                    }
                     hasPredicates = true;
                 }
             }
-        } finally {
-            WarningEvent.LISTENER.remove();
-        }
-        return stream;
+            return stream;
+        });
     }
 
     /**
@@ -314,7 +315,9 @@ final class FeatureStream extends DeferredStream<Feature> {
             projection = (FeatureProjection) mapper;
             return (Stream) this;
         }
-        return new PaginedStream<>(super.map(mapper), this);
+        final var stream = new PaginedStream<R>(super.map(mapper), this);
+        stream.listener = listener;
+        return stream;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
index 000c657f73..1edcfa8eac 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.java
@@ -99,9 +99,9 @@ public class Resources extends IndexedResourceBundle {
         public static final short IllegalQualifiedName_1 = 3;
 
         /**
-         * The literal of function “{0}” is not compatible with the reference 
system of property “{1}”.
+         * Function “{0}” does not accept the value of property “{1}”.
          */
-        public static final short IncompatibleLiteralCRS_2 = 18;
+        public static final short IncompatibleFunction_2 = 18;
 
         /**
          * Unexpected error while analyzing the database schema.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
index 15eaa17134..6895b1ea61 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources.properties
@@ -28,7 +28,7 @@ DataSource                        = Provider of connections 
to the database.
 DuplicatedColumn_1                = Unexpected duplication of column named 
\u201c{0}\u201d.
 DuplicatedSRID_2                  = Spatial Reference Identifier (SRID) {1} 
has more than one entry in \u201c{0}\u201d table.
 IllegalQualifiedName_1            = \u201c{0}\u201d is not a valid qualified 
name for a table.
-IncompatibleLiteralCRS_2          = The literal of function \u201c{0}\u201d is 
not compatible with the reference system of property \u201c{1}\u201d.
+IncompatibleFunction_2            = Function \u201c{0}\u201d does not accept 
the value of property \u201c{1}\u201d.
 InternalError                     = Unexpected error while analyzing the 
database schema.
 MalformedForeignerKey_2           = Unexpected column \u201c{1}\u201d in the 
\u201c{0}\u201d foreigner key.
 MappedSQLQueries                  = Resource names mapped to SQL queries.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
index 6f7724df8d..6673df2330 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/Resources_fr.properties
@@ -33,7 +33,7 @@ DataSource                        = Fournisseur de connexions 
\u00e0 la base de
 DuplicatedColumn_1                = Doublon inattendu d\u2019une colonne 
nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb.
 DuplicatedSRID_2                  = L\u2019identifiant de r\u00e9f\u00e9rence 
spatiale (SRID) {1} a plusieurs entr\u00e9s dans la table 
\u00ab\u202f{0}\u202f\u00bb.
 IllegalQualifiedName_1            = \u00ab\u202f{0}\u202f\u00bb n\u2019est pas 
un nom qualifi\u00e9 de table valide.
-IncompatibleLiteralCRS_2          = Le litt\u00e9ral de la fonction 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas compatible avec le syst\u00e8me de 
r\u00e9f\u00e9rence de la propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f\u00bb.
+IncompatibleFunction_2            = La fonction \u00ab\u202f{0}\u202f\u00bb 
n\u2019accepte pas la valeur de la propri\u00e9t\u00e9 
\u00ab\u202f{1}\u202f\u00bb.
 InternalError                     = Erreur inattendue pendant l\u2019analyse 
du sch\u00e9ma de la base de donn\u00e9es.
 MalformedForeignerKey_2           = Colonne \u00ab\u202f{1}\u202f\u00bb 
inattendue dans la cl\u00e9 \u00e9trang\u00e8re \u00ab\u202f{0}\u202f\u00bb.
 MappedSQLQueries                  = Noms de ressources associ\u00e9s \u00e0 
des requ\u00eates SQL.
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java
index 714cd8aacb..f429a52b75 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SelectionClause.java
@@ -21,9 +21,6 @@ import java.util.AbstractMap;
 import java.util.List;
 import java.util.ArrayList;
 import java.util.Optional;
-import java.util.logging.Level;
-import java.util.logging.LogRecord;
-import java.util.function.Consumer;
 import java.sql.Connection;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.extent.GeographicBoundingBox;
@@ -33,26 +30,22 @@ import org.apache.sis.geometry.WraparoundMethod;
 import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.geometry.wrapper.GeometryWrapper;
 import org.apache.sis.metadata.sql.internal.shared.SQLBuilder;
-import org.apache.sis.filter.internal.shared.WarningEvent;
 import org.apache.sis.referencing.CRS;
-import org.apache.sis.storage.FeatureSet;
-import org.apache.sis.system.Modules;
 import org.apache.sis.util.Workaround;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
-import org.opengis.util.CodeList;
 import org.opengis.feature.Feature;
 import org.opengis.filter.Filter;
 import org.opengis.filter.ValueReference;
 
 
 /**
- * Builder for the SQL fragment on the right side of the {@code WHERE} keyword.
+ * Builder for the <abbr>SQL</abbr> fragment on the right side of the {@code 
WHERE} keyword.
  *
  * @author  Alexis Manin (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
-public final class SelectionClause extends SQLBuilder implements 
Consumer<WarningEvent> {
+public final class SelectionClause extends SQLBuilder {
     /**
      * Whether the database rejects spatial functions that mix geometries with 
and without <abbr>CRS</abbr>.
      * We observed that PostGIS 3.4 produces an error not only when the 
geometry operands have different CRS,
@@ -328,42 +321,6 @@ public final class SelectionClause extends SQLBuilder 
implements Consumer<Warnin
         isInvalid = true;
     }
 
-    /**
-     * Returns the localized resources for warnings and error messages.
-     */
-    private Resources resources() {
-        return Resources.forLocale(table.database.listeners.getLocale());
-    }
-
-    /**
-     * Sets the logger, class and method names of the given record, then logs 
it.
-     * This method declares {@link FeatureSet#features(boolean)} as the public 
source of the log.
-     *
-     * @param  record  the record to configure and log.
-     */
-    private void log(final LogRecord record) {
-        record.setSourceClassName(FeatureSet.class.getName());
-        record.setSourceMethodName("features");
-        record.setLoggerName(Modules.SQL);
-        table.database.listeners.warning(record);
-    }
-
-    /**
-     * Invoked when a warning occurred during operations on filters or 
expressions.
-     *
-     * @param  event  the warning.
-     */
-    @Override
-    public void accept(final WarningEvent event) {
-        final LogRecord record = resources().createLogRecord(
-                Level.WARNING,
-                Resources.Keys.IncompatibleLiteralCRS_2,
-                
event.getOperatorType().flatMap(CodeList::identifier).orElse("?"),
-                
event.getParameter(ValueReference.class).map(ValueReference<?,?>::getXPath).orElse("?"));
-        record.setThrown(event.exception);
-        log(record);
-    }
-
     /**
      * Returns the <abbr>SQL</abbr> fragment built by this {@code 
SelectionClause}.
      * This method completes the information that we deferred until a 
connection is established.
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 793e8a9a2e..25c6ae8d5b 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
@@ -591,10 +591,9 @@ final class Table extends AbstractFeatureSet {
      *
      * @param  parallel  {@code true} for a parallel stream (if supported), or 
{@code false} for a sequential stream.
      * @return all features contained in this dataset.
-     * @throws DataStoreException if an error occurred while creating the 
stream.
      */
     @Override
-    public Stream<Feature> features(final boolean parallel) throws 
DataStoreException {
+    public Stream<Feature> features(final boolean parallel) {
         return new FeatureStream(this, parallel);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/util/stream/StreamWrapper.java
 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/util/stream/StreamWrapper.java
index 5c8981fe26..5225f44736 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/util/stream/StreamWrapper.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/util/stream/StreamWrapper.java
@@ -36,6 +36,7 @@ import java.util.stream.IntStream;
 import java.util.stream.LongStream;
 import java.util.stream.DoubleStream;
 import java.util.stream.Collector;
+import org.apache.sis.filter.internal.shared.WarningEvent;
 
 
 /**
@@ -51,7 +52,6 @@ import java.util.stream.Collector;
  *
  * @param  <T>  the type of objects contained in the stream, as specified in 
{@link Stream} interface.
  *
- *
  * @todo Add the methods that are new in JDK16.
  */
 public abstract class StreamWrapper<T> extends BaseStreamWrapper<T, Stream<T>> 
implements Stream<T> {
@@ -64,6 +64,12 @@ public abstract class StreamWrapper<T> extends 
BaseStreamWrapper<T, Stream<T>> i
      */
     Stream<T> source;
 
+    /**
+     * An optional listener to notify of warnings that occur during the 
execution of filters or expressions.
+     * This is always {@code null} by default and must be set explicitly if 
desired.
+     */
+    public Consumer<WarningEvent> listener;
+
     /**
      * Creates a new wrapper with initially no source.
      * The {@link #source} field should be initialized by subclass constructor.
@@ -124,193 +130,216 @@ public abstract class StreamWrapper<T> extends 
BaseStreamWrapper<T, Stream<T>> i
         }
     }
 
+    /**
+     * Executes the given action with a redirection of all warnings to the 
{@linkplain #listener}.
+     *
+     * @todo Replace by {@code ScopedValue.call(…)} when allowed to use JDK25.
+     *
+     * @param  <V>     the return value type of the given action.
+     * @param  action  the action to execute.
+     * @return the return value of the given action.
+     */
+    protected final <V> V execute(final Supplier<V> action) {
+        if (listener == null) {
+            return action.get();
+        }
+        final ThreadLocal<Consumer<WarningEvent>> context = 
WarningEvent.LISTENER;
+        final Consumer<WarningEvent> old = context.get();
+        try {
+            context.set(listener);
+            return action.get();
+        } finally {
+            context.set(old);
+        }
+    }
+
     /** Returns an equivalent stream that is parallel. */
     @Override public Stream<T> parallel() {
-        return update(source().parallel());
+        return execute(() -> update(source().parallel()));
     }
 
     /** Returns an equivalent stream that is sequential. */
     @Override public Stream<T> sequential() {
-        return update(source().sequential());
+        return execute(() -> update(source().sequential()));
     }
 
     /** Returns an equivalent stream that is unordered. */
     @Override public Stream<T> unordered() {
-        return update(source().unordered());
+        return execute(() -> update(source().unordered()));
     }
 
     /** Returns a stream with elements of this stream that match the given 
predicate. */
     @Override public Stream<T> filter(Predicate<? super T> predicate) {
-        return update(source().filter(predicate));
+        return execute(() -> update(source().filter(predicate)));
     }
 
     /** Returns a stream with results of applying the given function to the 
elements of this stream. */
     @Override public <R> Stream<R> map(Function<? super T, ? extends R> 
mapper) {
-        return delegate().map(mapper);
+        return execute(() -> delegate().map(mapper));
     }
 
     /** Returns a stream with results of applying the given function to the 
elements of this stream. */
     @Override public IntStream mapToInt(ToIntFunction<? super T> mapper) {
-        return delegate().mapToInt(mapper);
+        return execute(() -> delegate().mapToInt(mapper));
     }
 
     /** Returns a stream with results of applying the given function to the 
elements of this stream. */
     @Override public LongStream mapToLong(ToLongFunction<? super T> mapper) {
-        return delegate().mapToLong(mapper);
+        return execute(() -> delegate().mapToLong(mapper));
     }
 
     /** Returns a stream with results of applying the given function to the 
elements of this stream. */
     @Override public DoubleStream mapToDouble(ToDoubleFunction<? super T> 
mapper) {
-        return delegate().mapToDouble(mapper);
+        return execute(() -> delegate().mapToDouble(mapper));
     }
 
     /** Returns a stream with results of applying the given function to the 
elements of this stream. */
     @Override public <R> Stream<R> flatMap(Function<? super T, ? extends 
Stream<? extends R>> mapper) {
-        return delegate().flatMap(mapper);
+        return execute(() -> delegate().flatMap(mapper));
     }
 
     /** Returns a stream with results of applying the given function to the 
elements of this stream. */
     @Override public IntStream flatMapToInt(Function<? super T, ? extends 
IntStream> mapper) {
-        return delegate().flatMapToInt(mapper);
+        return execute(() -> delegate().flatMapToInt(mapper));
     }
 
     /** Returns a stream with results of applying the given function to the 
elements of this stream. */
     @Override public LongStream flatMapToLong(Function<? super T, ? extends 
LongStream> mapper) {
-        return delegate().flatMapToLong(mapper);
+        return execute(() -> delegate().flatMapToLong(mapper));
     }
 
     /** Returns a stream with results of applying the given function to the 
elements of this stream. */
     @Override public DoubleStream flatMapToDouble(Function<? super T, ? 
extends DoubleStream> mapper) {
-        return delegate().flatMapToDouble(mapper);
+        return execute(() -> delegate().flatMapToDouble(mapper));
     }
 
     /** Returns a stream with distinct elements of this stream. */
     @Override public Stream<T> distinct() {
-        return update(source().distinct());
+        return execute(() -> update(source().distinct()));
     }
 
     /** Returns a stream with elements of this stream sorted in natural order. 
*/
     @Override public Stream<T> sorted() {
-        return update(source().sorted());
+        return execute(() -> update(source().sorted()));
     }
 
     /** Returns a stream with elements of this stream sorted using the given 
comparator. */
     @Override public Stream<T> sorted(Comparator<? super T> comparator) {
-        return update(source().sorted(comparator));
+        return execute(() -> update(source().sorted(comparator)));
     }
 
     /** Returns a stream performing the specified action on each element when 
consumed. */
     @Override public Stream<T> peek(Consumer<? super T> action) {
-        return update(source().peek(action));
+        return execute(() -> update(source().peek(action)));
     }
 
     /** Returns a stream with truncated at the given number of elements. */
     @Override public Stream<T> limit(long maxSize) {
-        return update(source().limit(maxSize));
+        return execute(() -> update(source().limit(maxSize)));
     }
 
     /** Returns a stream discarding the specified number of elements. */
     @Override public Stream<T> skip(long n) {
-        return update(source().skip(n));
+        return execute(() -> update(source().skip(n)));
     }
 
     /** Performs an action for each element of this stream. */
     @Override public void forEach(Consumer<? super T> action) {
-        source().forEach(action);
+        execute(() -> {source().forEach(action); return null;});
     }
 
     /** Performs an action for each element of this stream in encounter order. 
*/
     @Override public void forEachOrdered(Consumer<? super T> action) {
-        source().forEachOrdered(action);
+        execute(() -> {source().forEachOrdered(action); return null;});
     }
 
     /** Performs a reduction on the elements of this stream. */
     @Override public T reduce(T identity, BinaryOperator<T> accumulator) {
-        return source().reduce(identity, accumulator);
+        return execute(() -> source().reduce(identity, accumulator));
     }
 
     /** Performs a reduction on the elements of this stream. */
     @Override public Optional<T> reduce(BinaryOperator<T> accumulator) {
-        return source().reduce(accumulator);
+        return execute(() -> source().reduce(accumulator));
     }
 
     /** Performs a reduction on the elements of this stream. */
     @Override public <U> U reduce(U identity, BiFunction<U, ? super T, U> 
accumulator, BinaryOperator<U> combiner) {
-        return source().reduce(identity, accumulator, combiner);
+        return execute(() -> source().reduce(identity, accumulator, combiner));
     }
 
     /** Performs a mutable reduction on the elements of this stream. */
     @Override public <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super 
T> accumulator, BiConsumer<R, R> combiner) {
-        return source().collect(supplier, accumulator, combiner);
+        return execute(() -> source().collect(supplier, accumulator, 
combiner));
     }
 
     /** Performs a mutable reduction on the elements of this stream. */
     @Override public <R, A> R collect(Collector<? super T, A, R> collector) {
-        return source().collect(collector);
+        return execute(() -> source().collect(collector));
     }
 
     /** Returns the minimum element of this stream according to the provided 
comparator. */
     @Override public Optional<T> min(Comparator<? super T> comparator) {
-        return source().min(comparator);
+        return execute(() -> source().min(comparator));
     }
 
     /** Returns the maximum element of this stream according to the provided 
comparator. */
     @Override public Optional<T> max(Comparator<? super T> comparator) {
-        return source().max(comparator);
+        return execute(() -> source().max(comparator));
     }
 
     /** Returns the number of elements in this stream. */
     @Override public long count() {
-        return source().count();
+        return execute(() -> source().count());
     }
 
     /** Returns whether at least one element of this stream matches the 
provided predicate. */
     @Override public boolean anyMatch(Predicate<? super T> predicate) {
-        return source().anyMatch(predicate);
+        return execute(() -> source().anyMatch(predicate));
     }
 
     /** Returns whether all elements of this stream match the provided 
predicate. */
     @Override public boolean allMatch(Predicate<? super T> predicate) {
-        return source().allMatch(predicate);
+        return execute(() -> source().allMatch(predicate));
     }
 
     /** Returns whether none element of this stream match the provided 
predicate. */
     @Override public boolean noneMatch(Predicate<? super T> predicate) {
-        return source().noneMatch(predicate);
+        return execute(() -> source().noneMatch(predicate));
     }
 
     /** Returns the first element of this stream. */
     @Override public Optional<T> findFirst() {
-        return source().findFirst();
+        return execute(() -> source().findFirst());
     }
 
     /** Returns any element of this stream. */
     @Override public Optional<T> findAny() {
-        return source().findAny();
+        return execute(() -> source().findAny());
     }
 
     /** Returns an iterator for the elements of this stream. */
     @Override public Iterator<T> iterator() {
-        return source().iterator();
+        return execute(() -> source().iterator());
     }
 
     /** Returns a spliterator for the elements of this stream. */
     @Override public Spliterator<T> spliterator() {
-        return source().spliterator();
+        return execute(() -> source().spliterator());
     }
 
     /** Returns all elements in an array. */
     @Override public Object[] toArray() {
-        return source().toArray();
+        return execute(() -> source().toArray());
     }
 
     /** Returns all elements in an array. */
     @Override public <A> A[] toArray(IntFunction<A[]> generator) {
-        return source().toArray(generator);
+        return execute(() -> source().toArray(generator));
     }
 
     /** Returns an equivalent stream with an additional close handler. */
     @Override public Stream<T> onClose(Runnable closeHandler) {
-        return update(source().onClose(closeHandler));
+        return execute(() -> update(source().onClose(closeHandler)));
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
index cedd0166ec..f71123d66c 100644
--- 
a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java
@@ -101,7 +101,7 @@ public final class SQLStoreTest extends TestOnAllDatabases {
     /**
      * Factory to use for creating filter objects.
      */
-    private final FilterFactory<Feature,Object,Object> FF;
+    private final FilterFactory<Feature, ?, ?> FF;
 
     /**
      * Creates a new test.
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 d4e304c81b..25fbc34507 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
@@ -51,7 +51,7 @@ public final class SelectionClauseWriterTest extends TestCase 
implements SchemaM
     /**
      * The factory to use for creating the filter objects.
      */
-    private final FilterFactory<Feature,Object,Object> FF;
+    private final FilterFactory<Feature, Object, ?> FF;
 
     /**
      * A dummy table for testing purpose.
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 39180d69b6..df2843b599 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
@@ -205,6 +205,7 @@ public class JoinFeatureSet extends AggregatedFeatureSet {
      * @param  featureInfo  information about the {@link FeatureType} of this 
feature set.
      * @throws DataStoreException if an error occurred while creating the 
feature set.
      */
+    @SuppressWarnings("LocalVariableHidesMemberVariable")
     public JoinFeatureSet(final Resource parent,
                           final FeatureSet left,  String leftAlias,
                           final FeatureSet right, String rightAlias,
@@ -236,9 +237,7 @@ public class JoinFeatureSet extends AggregatedFeatureSet {
             new DefaultAssociationRole(properties(rightAlias), rightType, 
joinType.minimumOccurs(true),  1)
         };
         final String identifierDelimiter = Containers.property(featureInfo, 
"identifierDelimiter", String.class);
-        if (identifierDelimiter != null && 
AttributeConvention.hasIdentifier(leftType)
-                                        && 
AttributeConvention.hasIdentifier(rightType))
-        {
+        if (identifierDelimiter != null && hasIdentifier(leftType) && 
hasIdentifier(rightType)) {
             final Operation identifier = FeatureOperations.compound(
                     properties(AttributeConvention.IDENTIFIER_PROPERTY), 
identifierDelimiter,
                     Containers.property(featureInfo, "identifierPrefix", 
String.class),
@@ -252,6 +251,14 @@ public class JoinFeatureSet extends AggregatedFeatureSet {
         type = new DefaultFeatureType(featureInfo, false, null, properties);
     }
 
+    /**
+     * Returns whether the given feature type has a {@value 
AttributeConvention#IDENTIFIER} property.
+     */
+    private static boolean hasIdentifier(final FeatureType feature) {
+        return feature.hasProperty(AttributeConvention.IDENTIFIER) &&
+               feature.getProperty(AttributeConvention.IDENTIFIER) != null;
+    }
+
     /**
      * Creates a minimal {@code properties} map for feature type or property 
type constructors.
      * This minimalist map contain only the mandatory entry, which is the name.
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 4642c8380d..1897df8c5e 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
@@ -273,12 +273,9 @@ public final class FeatureQueryTest extends TestCase {
         final PropertyType pt1 = resultType.getProperty("value1");
         final PropertyType pt2 = resultType.getProperty("renamed1");
         final PropertyType pt3 = resultType.getProperty("computed");
-        assertTrue(pt1 instanceof AttributeType);
-        assertTrue(pt2 instanceof AttributeType);
-        assertTrue(pt3 instanceof AttributeType);
-        assertEquals(Integer.class, ((AttributeType) pt1).getValueClass());
-        assertEquals(Integer.class, ((AttributeType) pt2).getValueClass());
-        assertEquals(String.class,  ((AttributeType) pt3).getValueClass());
+        assertEquals(Integer.class, assertInstanceOf(AttributeType.class, 
pt1).getValueClass());
+        assertEquals(Integer.class, assertInstanceOf(AttributeType.class, 
pt2).getValueClass());
+        assertEquals(String.class,  assertInstanceOf(AttributeType.class, 
pt3).getValueClass());
 
         // Check feature instance.
         assertEquals(3, instance.getPropertyValue("value1"));
@@ -349,14 +346,12 @@ public final class FeatureQueryTest extends TestCase {
         assertEquals(2, resultType.getProperties(true).size());
         final PropertyType pt1 = resultType.getProperty("value1");
         final PropertyType pt2 = resultType.getProperty("unexpected");
-        assertTrue(pt1 instanceof AttributeType<?>);
-        assertTrue(pt2 instanceof AttributeType<?>);
-        assertEquals(Integer.class, ((AttributeType<?>) pt1).getValueClass());
-        assertEquals(Object.class,  ((AttributeType<?>) pt2).getValueClass());
+        assertEquals(Integer.class, assertInstanceOf(AttributeType.class, 
pt1).getValueClass());
+        assertEquals( Object.class, assertInstanceOf(AttributeType.class, 
pt2).getValueClass());
 
         // Check feature property values.
-        assertEquals(3,    instance.getPropertyValue("value1"));
-        assertEquals(null, instance.getPropertyValue("unexpected"));
+        assertEquals(3, instance.getPropertyValue("value1"));
+        assertNull(instance.getPropertyValue("unexpected"));
     }
 
     /**
@@ -423,16 +418,11 @@ public final class FeatureQueryTest extends TestCase {
         final PropertyType pt1 = resultType.getProperty("value1");
         final PropertyType pt2 = resultType.getProperty("renamed1");
         final PropertyType pt3 = resultType.getProperty("computed");
-        assertTrue(pt1 instanceof AttributeType<?>);
-        assertTrue(pt2 instanceof Operation);
-        assertTrue(pt3 instanceof Operation);
-        final IdentifiedType result2 = ((Operation) pt2).getResult();
-        final IdentifiedType result3 = ((Operation) pt3).getResult();
-        assertEquals(Integer.class, ((AttributeType<?>) pt1).getValueClass());
-        assertTrue(result2 instanceof AttributeType<?>);
-        assertTrue(result3 instanceof AttributeType<?>);
-        assertEquals(Integer.class, ((AttributeType<?>) 
result2).getValueClass());
-        assertEquals(String.class,  ((AttributeType<?>) 
result3).getValueClass());
+        final IdentifiedType result2 = assertInstanceOf(Operation.class, 
pt2).getResult();
+        final IdentifiedType result3 = assertInstanceOf(Operation.class, 
pt3).getResult();
+        assertEquals(Integer.class, assertInstanceOf(AttributeType.class, 
pt1).getValueClass());
+        assertEquals(Integer.class, assertInstanceOf(AttributeType.class, 
result2).getValueClass());
+        assertEquals( String.class, assertInstanceOf(AttributeType.class, 
result3).getValueClass());
 
         // Check feature instance.
         assertEquals(3, instance.getPropertyValue("value1"));
diff --git 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/aggregate/JoinFeatureSetTest.java
 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/aggregate/JoinFeatureSetTest.java
index a284783097..84a431d15a 100644
--- 
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/aggregate/JoinFeatureSetTest.java
+++ 
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/aggregate/JoinFeatureSetTest.java
@@ -17,7 +17,6 @@
 package org.apache.sis.storage.aggregate;
 
 import java.util.Map;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Iterator;
 import java.util.stream.Collectors;
@@ -48,6 +47,7 @@ import org.opengis.filter.MatchAction;
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class JoinFeatureSetTest extends TestCase {
     /**
      * The set of features to be joined together.
@@ -118,15 +118,13 @@ public final class JoinFeatureSetTest extends TestCase {
      * Creates a new join feature set of the given type using the {@link 
#featureSet1} and {@link #featureSet2}.
      */
     private FeatureSet create(final JoinFeatureSet.Type type) throws 
DataStoreException {
-        final FilterFactory<Feature, Object, ?> factory = 
DefaultFilterFactory.forFeatures();
+        final FilterFactory<Feature, ?, ?> factory = 
DefaultFilterFactory.forFeatures();
         final BinaryComparisonOperator<Feature> condition = factory.equal(
                 factory.property("att2", String.class),
                 factory.property("att3", String.class),
                 true, MatchAction.ANY);
-        final Map<String,Object> properties = new HashMap<>(4);
-        assertNull(properties.put("name", "JoinSet"));
-        assertNull(properties.put("identifierDelimiter", " "));
-        return new JoinFeatureSet(null, featureSet1, "s1", featureSet2, "s2", 
type, condition, properties);
+        return new JoinFeatureSet(null, featureSet1, "s1", featureSet2, "s2", 
type, condition,
+                                  Map.of("name", "JoinSet", 
"identifierDelimiter", " "));
     }
 
     /**
diff --git a/geoapi/snapshot b/geoapi/snapshot
index 7eff7cfcbd..e8dfb3b92a 160000
--- a/geoapi/snapshot
+++ b/geoapi/snapshot
@@ -1 +1 @@
-Subproject commit 7eff7cfcbd6ce7ac82b99a90b9ee465914e629c1
+Subproject commit e8dfb3b92a0141221d33991c197b3f74bb0831c9
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 06797449e3..425971bf5e 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
@@ -45,6 +45,7 @@ import org.apache.sis.feature.builder.FeatureTypeBuilder;
 import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.feature.internal.shared.AttributeConvention;
+import org.apache.sis.filter.internal.shared.WarningEvent;
 import org.apache.sis.storage.FeatureQuery;
 import org.apache.sis.storage.Aggregate;
 import org.apache.sis.storage.DataStoreException;
@@ -63,6 +64,8 @@ import org.apache.sis.util.iso.Names;
 // Test dependencies
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
 import static org.junit.jupiter.api.Assertions.*;
 import org.junit.jupiter.api.parallel.Execution;
 import org.junit.jupiter.api.parallel.ExecutionMode;
@@ -84,6 +87,26 @@ public class SEPortrayerTest {
     private final FeatureSet fishes;
     private final FeatureSet boats;
 
+    /**
+     * Shutdown the warnings that occur during the execution of expressions or 
filters.
+     *
+     * @todo Investigate why we get those warnings.
+     */
+    @BeforeEach
+    public void disableFilterWarnings() {
+        WarningEvent.LISTENER.set((event) -> {});
+    }
+
+    /**
+     * Shutdown the warnings that occur during the execution of expressions or 
filters.
+     *
+     * @todo Investigate why we get those warnings.
+     */
+    @AfterEach
+    public void resetFilterWarnings() {
+        WarningEvent.LISTENER.remove();
+    }
+
     /**
      * Creates a new test case.
      */
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ExpandedFeature.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ExpandedFeature.java
index a67208b599..67ab103a13 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ExpandedFeature.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/dataset/ExpandedFeature.java
@@ -169,14 +169,6 @@ final class ExpandedFeature implements Feature {
                : List.of();
     }
 
-    /**
-     * Synonymous of {@link #getPropertyValue(String)} since we do not check 
property existence.
-     */
-    @Override
-    public Object getValueOrFallback(final String name, final Object 
missingPropertyFallback) {
-        return getPropertyValue(name);
-    }
-
     /**
      * Unsupported operation.
      */


Reply via email to