This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 781fc115d07e3747bdbae986df09984efa4a31a4
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Dec 29 20:22:04 2025 +0100

    Add dynamic optimization which will optimize a filter or expression when 
the feature type become known.
    It handles the case where a stream of feature instances may provide many 
different feature types.
---
 .../org/apache/sis/filter/AssociationValue.java    |   2 +-
 .../apache/sis/filter/BinaryGeometryFilter.java    |   2 +-
 .../org/apache/sis/filter/DynamicOptimization.java | 352 +++++++++++++++++++++
 .../org/apache/sis/filter/IdentifierFilter.java    |   5 +-
 .../main/org/apache/sis/filter/Optimization.java   | 245 +++++++++++---
 .../main/org/apache/sis/filter/PropertyValue.java  |  10 +-
 .../org/apache/sis/filter/base/BinaryFunction.java |   1 +
 .../org/apache/sis/filter/base/WarningEvent.java   |  15 +-
 .../org/apache/sis/filter/sqlmm/TwoGeometries.java |   2 +-
 .../apache/sis/feature/AbstractFeatureTest.java    |   4 +-
 .../apache/sis/filter/DynamicOptimizationTest.java | 176 +++++++++++
 .../org/apache/sis/filter/LogicalFilterTest.java   |   4 +-
 12 files changed, 751 insertions(+), 67 deletions(-)

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 bf68949e9f..2f45bcbda7 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
@@ -214,7 +214,7 @@ walk:   if (instance != null) {
                 }
             }
         } catch (PropertyNotFoundException e) {
-            warning(e, true);
+            optimization.warning(e, true);
         }
         return this;
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
index b8b50751c0..6411aaf762 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/BinaryGeometryFilter.java
@@ -223,7 +223,7 @@ abstract class BinaryGeometryFilter<R> extends Node 
implements SpatialOperator<R
                     else effective2 = literal;
                 }
             } catch (PropertyNotFoundException | TransformException e) {
-                warning(e, true);
+                optimization.warning(e, false);
             }
             /*
              * If one of the "effective" parameter has been modified, recreate 
a new filter.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DynamicOptimization.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DynamicOptimization.java
new file mode 100644
index 0000000000..37eae3a606
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/DynamicOptimization.java
@@ -0,0 +1,352 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.List;
+import java.util.Map;
+import java.util.IdentityHashMap;
+import org.opengis.util.CodeList;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.filter.Filter;
+import org.opengis.filter.Expression;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.util.ScopedName;
+
+
+/**
+ * Optimization of a filter or expression for feature types that are 
discovered when the filter or expression
+ * is executed. This is needed when a feature type may have subtypes, because 
it may contain properties that
+ * are unknown to the base type.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @param  <T>  either {@link Filter} or {@link Expression}.
+ */
+abstract class DynamicOptimization<T> {
+    /**
+     * The filters or expressions that have already been optimized.
+     */
+    private final Map<FeatureType, T> cache;
+
+    /**
+     * Creates a new dynamic optimization.
+     */
+    protected DynamicOptimization() {
+        cache = new IdentityHashMap<>();
+    }
+
+    /**
+     * Returns the filter or expression to use for executing this operation
+     * on feature instances of the given type.
+     *
+     * <h4>Implementation note</h4>
+     * We could use {@link java.util.concurrent.ConcurrentHashMap} instead of 
synchronization,
+     * but the map is expected to be very small, often with only one element. 
For such small maps,
+     * the additional complexity of concurrent maps may be greater than the 
synchronization cost.
+     * Furthermore, subclasses have an optimization which will avoid this 
synchronization in the
+     * common case where consecutive feature instances have the same feature 
type.
+     *
+     * @param  type  type of the feature instance on which this operation will 
be executed.
+     * @return the filter or expression to use for feature instances of the 
given type.
+     */
+    protected final T delegate(final FeatureType type) {
+        synchronized (cache) {
+            return cache.computeIfAbsent(type, (key) -> {
+                var optimizer = new Optimization();
+                optimizer.setFinalFeatureType(key);
+                return optimizeOriginal(optimizer);
+            });
+        }
+    }
+
+    /**
+     * Optimizes the original filter or expressions using the given setting.
+     *
+     * @param  optimizer  the optimization to apply on this filter or 
expression.
+     * @return the optimized filter or expression, or {@link #original} if no 
change.
+     */
+    protected abstract T optimizeOriginal(final Optimization optimizer);
+
+    /**
+     * Returns a dynamic optimization of the given filter.
+     * If dynamic optimization is already enabled, then the filter is returned 
as-is.
+     *
+     * @param  original  the filter for which to provide dynamic optimization.
+     * @return filter equivalent to the given filter, but with dynamic 
optimization enabled.
+     */
+    public static Filter<Feature> of(final Filter<Feature> original) {
+        if (original instanceof OfFilter) {
+            return original;
+        }
+        return new OfFilter(original);
+    }
+
+    /**
+     * Returns a dynamic optimization of the given filter if that filter 
accepts feature instances.
+     * If dynamic optimization is already enabled or is not applicable, then 
the filter is returned as-is.
+     *
+     * @param  <R>       type of resources accepted by the filter.
+     * @param  original  the filter for which to provide dynamic optimization.
+     * @return filter equivalent to the given filter, but with dynamic 
optimization enabled.
+     */
+    @SuppressWarnings("unchecked")
+    public static <R> Filter<R> ofAny(final Filter<R> original) {
+        if (original.getResourceClass().isAssignableFrom(Feature.class)) {
+            return of((Filter) original);
+        }
+        return original;
+    }
+
+    /**
+     * Dynamic optimization of filters.
+     */
+    private static final class OfFilter extends 
DynamicOptimization<Filter<Feature>>
+            implements Optimization.OnFilter<Feature>
+    {
+        /**
+         * The original (without optimization) filter.
+         */
+        private final Filter<Feature> original;
+
+        /**
+         * Most recently used entry of {@link #cache}. This is an optimization 
for the
+         * common case where many consecutive features are instances of the 
same type.
+         * There is no need for {@code volatile} keyword if all fields are 
final.
+         */
+        private Entry last;
+
+        /**
+         * A thread-safe (type, filter) entry. Thread-safety is achieved by 
making sure that all fields are final.
+         * The declared type of {@link #delegate} is {@code Filter} rather 
than {@code <T>} for reducing the number
+         * of casts.
+         */
+        private static final class Entry {
+            /** The type of feature instances expected by {@link #delegate}. */
+            final FeatureType type;
+
+            /** The actual filter to apply on features instances. */
+            final Filter<Feature> delegate;
+
+            /** Associates the actual filter to feature instances of the given 
type. */
+            Entry(final FeatureType type, final Filter<Feature> delegate) {
+                this.type = type;
+                this.delegate = delegate;
+            }
+        }
+
+        /**
+         * Creates a new dynamic optimization.
+         *
+         * @param  original  the original (without optimization) filter.
+         */
+        OfFilter(final Filter<Feature> original) {
+            this.original = original;
+            last = new Entry(null, original);
+        }
+
+        /**
+         * Optimizes the original filter using the given setting.
+         * The target feature type shall be set by the caller before to invoke 
this method.
+         *
+         * @param  optimizer  the optimization to apply on the original filter.
+         * @return the optimized filter, or {@link #original} if no change.
+         */
+        @Override
+        protected Filter<Feature> optimizeOriginal(final Optimization 
optimizer) {
+            return optimizer.apply(original);
+        }
+
+        /**
+         * Optimizes this filter using the given setting.
+         *
+         * @param  optimizer  the optimization to apply on this filter.
+         * @return the optimized filter, or {@code this} if no change.
+         */
+        @Override
+        public Filter<Feature> optimize(final Optimization optimizer) {
+            var result = optimizeOriginal(optimizer);
+            return (result != original) ? of(result) : this;
+        }
+
+        /**
+         * Filters the given feature instance by delegating to a filter 
optimized for the actual feature type.
+         *
+         * @param  feature  the feature instance to test.
+         * @return whether the given feature instance is accepted.
+         */
+        @Override
+        public boolean test(final Feature feature) {
+            final FeatureType type = feature.getType();
+            Entry entry = last;
+            if (entry.type != type) {
+                last = entry = new Entry(type, delegate(type));
+            }
+            return entry.delegate.test(feature);
+        }
+
+        /** Delegates to the original filter. */
+        @Override public CodeList<?>                  getOperatorType()  
{return original.getOperatorType();}
+        @Override public Class<? super Feature>       getResourceClass() 
{return original.getResourceClass();}
+        @Override public List<Expression<Feature, ?>> getExpressions()   
{return original.getExpressions();}
+        @Override public String                       toString()         
{return original.toString();}
+    }
+
+    /**
+     * Returns a dynamic optimization of the given expression.
+     * If dynamic optimization is already enabled, then the expression is 
returned as-is.
+     *
+     * @param  original  the expression for which to provide dynamic 
optimization.
+     * @return expression equivalent to the given expression, but with dynamic 
optimization enabled.
+     */
+    public static <V> Expression<Feature, V> of(final Expression<Feature, V> 
original) {
+        if (original instanceof OfExpression<?>) {
+            return original;
+        }
+        return new OfExpression<>(original);
+    }
+
+    /**
+     * Returns a dynamic optimization of the given expression if that 
expression accepts feature instances.
+     * If dynamic optimization is already enabled or is not applicable, then 
the expression is returned as-is.
+     *
+     * @param  <R>       type of resources accepted by the expression.
+     * @param  <V>       type of values returned by the expression.
+     * @param  original  the expression for which to provide dynamic 
optimization.
+     * @return expression equivalent to the given expression, but with dynamic 
optimization enabled.
+     */
+    @SuppressWarnings("unchecked")
+    public static <R,V> Expression<R,V> ofAny(final Expression<R,V> original) {
+        if (original.getResourceClass().isAssignableFrom(Feature.class)) {
+            return of((Expression) original);
+        }
+        return original;
+    }
+
+    /**
+     * Dynamic optimization of expressions.
+     *
+     * @param  <V>  the type of values computed by the expression.
+     */
+    private static final class OfExpression<V> extends 
DynamicOptimization<Expression<Feature, ? extends V>>
+            implements Optimization.OnExpression<Feature, V>
+    {
+        /**
+         * The original (without optimization) expression.
+         */
+        private final Expression<Feature, ? extends V> original;
+
+        /**
+         * Most recently used entry of {@link #cache}. This is an optimization 
for the
+         * common case where many consecutive features are instances of the 
same type.
+         * There is no need for {@code volatile} keyword if all fields are 
final.
+         */
+        private Entry<V> last;
+
+        /**
+         * A thread-safe (type, expression) entry. Thread-safety is achieved 
by making sure that all fields are final.
+         * The declared type of {@link #delegate} is {@code Expression} rather 
than {@code <T>} for reducing the number
+         * of casts.
+         */
+        private static final class Entry<V> {
+            /** The type of feature instances expected by {@link #delegate}. */
+            final FeatureType type;
+
+            /** The actual expression to apply on features instances. */
+            final Expression<Feature, ? extends V> delegate;
+
+            /** Associates the actual expression to feature instances of the 
given type. */
+            Entry(final FeatureType type, final Expression<Feature, ? extends 
V> delegate) {
+                this.type = type;
+                this.delegate = delegate;
+            }
+        }
+
+        /**
+         * Creates a new dynamic optimization.
+         *
+         * @param  original  the expression for which to provide dynamic 
optimization.
+         */
+        OfExpression(final Expression<Feature, ? extends V> original) {
+            this.original = original;
+            last = new Entry<>(null, original);
+        }
+
+        /**
+         * Optimizes the original expressions using the given setting.
+         * The target feature type shall be set by the caller before to invoke 
this method.
+         *
+         * @param  optimizer  the optimization to apply on the original 
expression.
+         * @return the optimized expression, or {@link #original} if no change.
+         */
+        @Override
+        protected Expression<Feature, ? extends V> optimizeOriginal(final 
Optimization optimizer) {
+            return optimizer.apply(original);
+        }
+
+        /**
+         * Optimizes this expressions using the given setting.
+         *
+         * @param  optimizer  the optimization to apply on this expression.
+         * @return the optimized expression, or {@code this} if no change.
+         */
+        @Override
+        public Expression<Feature, ? extends V> optimize(final Optimization 
optimizer) {
+            var result = optimizeOriginal(optimizer);
+            return (result != original) ? of(result) : this;
+        }
+
+        /**
+         * Evaluates the given feature instance by delegating to an expression 
optimized for the actual feature type.
+         *
+         * @param  feature  the feature instance to evaluate.
+         * @return the value evaluated from the given feature.
+         */
+        @Override
+        public V apply(final Feature feature) {
+            final FeatureType type = feature.getType();
+            Entry<V> entry = last;
+            if (entry.type != type) {
+                last = entry = new Entry<>(type, delegate(type));
+            }
+            return entry.delegate.apply(feature);
+        }
+
+        /**
+         * Returns an expression doing the same evaluation as this method, but 
returning results
+         * as values of the specified type.
+         *
+         * @param  <N>     compile-time value of {@code target} type.
+         * @param  target  desired type of expression results.
+         * @return expression doing the same operation this this expression 
but with results of the specified type.
+         * @throws ClassCastException if the specified type is not a target 
type supported by implementation.
+         */
+        @Override
+        @SuppressWarnings("unchecked")
+        public <N> Expression<Feature, N> toValueType(Class<N> target) {
+            Expression<Feature, N> other = original.toValueType(target);
+            return (other != original) ? of(other) : (Expression<Feature, N>) 
this;
+        }
+
+        /** Delegates to the original expression. */
+        @Override public ScopedName                   getFunctionName()  
{return original.getFunctionName();}
+        @Override public Class<? super Feature>       getResourceClass() 
{return original.getResourceClass();}
+        @Override public List<Expression<Feature, ?>> getParameters()    
{return original.getParameters();}
+        @Override public String                       toString()         
{return original.toString();}
+    }
+}
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 80b3ecb877..72c7c1dc24 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
@@ -90,8 +90,9 @@ final class IdentifierFilter extends Node
                 return new IdentifierFilter(this, preferredName);
             }
         } catch (PropertyNotFoundException e) {
-            warning(e, true);
-            if (found.isEmpty()) {
+            boolean resolved = found.isEmpty();
+            optimization.warning(e, !resolved);
+            if (resolved) {
                 return Filter.exclude();    // The property does not exist in 
any feature type.
             }
         }
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 4b796616a9..ad8f2c6fc1 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
@@ -22,12 +22,15 @@ import java.util.List;
 import java.util.Iterator;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Collection;
 import java.util.IdentityHashMap;
 import java.util.ConcurrentModificationException;
 import java.util.Optional;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.BiPredicate;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.feature.internal.shared.AttributeConvention;
 import org.apache.sis.feature.Features;
@@ -35,7 +38,9 @@ import org.apache.sis.geometry.wrapper.Geometries;
 import org.apache.sis.math.FunctionProperty;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.Containers;
+import org.apache.sis.util.logging.Logging;
 import org.apache.sis.filter.base.Node;
+import org.apache.sis.filter.base.WarningEvent;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.util.CodeList;
@@ -122,6 +127,25 @@ public class Optimization {
      */
     private Map<Object, Object> done;
 
+    /**
+     * The filter or expression in process of being optimized.
+     * This is used by {@link #warning(Exception)} for reporting the source of 
errors.
+     */
+    private Object currentFilterOrExpression;
+
+    /**
+     * Set during the execution of an {@code apply(…)} method if it could do 
better.
+     * If {@code true}, then invoking {@code apply(…)} again with, for 
example, a single
+     * {@link #setFinalFeatureType final feature type} may improve the result 
efficiency.
+     * Conversely, a value of {@code false} means that no improvement is 
expected.
+     *
+     * @see #apply(Filter)
+     * @see #apply(Expression)
+     * @see #warning(Exception, boolean)
+     * @see #isImprovable()
+     */
+    private boolean isImprovable;
+
     /**
      * Creates a new instance.
      */
@@ -319,6 +343,106 @@ public class Optimization {
         return Optional.ofNullable(crs);
     }
 
+    /**
+     * Returns whether an {@code apply(…)} method may obtain a better result 
with dynamic optimization.
+     * A value of {@code true} does not necessarily mean that {@link 
DynamicOptimization} will be created,
+     * because {@code DynamicOptimization.ofAny(…)} will check if the resource 
type is {@code Feature}.
+     *
+     * @param  source         the filter or expression to test if it can be 
improved.
+     * @param  isOptimizable  one of the {@code isOptimizable(…)} methods.
+     * @return whether it is suggested to use {@link DynamicOptimization}.
+     */
+    private <T> boolean isImprovable(final T source, final BiPredicate<T, 
Set<Object>> isOptimizable) {
+        if (isImprovable || featureTypes.isEmpty()) {
+            return isOptimizable.test(source, new HashSet<>());
+        }
+        return false;
+    }
+
+    /**
+     * Verifies whether the given filter can be optimized.
+     *
+     * @param  filter  the filter to test.
+     * @param  done    an initially empty set used as a safety against 
infinite recursion.
+     * @return whether the given filter can be optimized.
+     */
+    private static boolean isOptimizable(final Filter<?> filter, final 
Set<Object> done) {
+        if (filter instanceof OnFilter<?>) {
+            return true;
+        }
+        for (Expression<?,?> parameter : filter.getExpressions()) {
+            if (done.add(parameter) && isOptimizable(parameter, done)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Verifies whether the given expression can be optimized.
+     *
+     * @param  expression  the expression to test.
+     * @param  done        an initially empty set used as a safety against 
infinite recursion.
+     * @return whether the given expression can be optimized.
+     */
+    private static boolean isOptimizable(final Expression<?,?> expression, 
final Set<Object> done) {
+        if (expression instanceof OnExpression<?,?>) {
+            return true;
+        }
+        for (Expression<?,?> parameter : expression.getParameters()) {
+            if (done.add(parameter) && isOptimizable(parameter, done)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns whether a call to {@code apply(…)} is the first of possibly 
recursive calls.
+     * This method shall be invoked in all {@code apply(…)} methods before to 
do the optimization.
+     *
+     * @return whether this is the entry (non-recursive) call to an {@code 
apply(…)} method.
+     */
+    private boolean isFirstCall() {
+        if (done != null) {
+            return false;
+        }
+        isImprovable = false;
+        done = new IdentityHashMap<>();
+        return true;
+    }
+
+    /**
+     * Returns the previous optimization result for the given filter or 
expression.
+     * The {@link #isFirstCall()} method must have been invoked before this 
method.
+     *
+     * @param  original        the filter or expression to optimize.
+     * @param  identification  method to invoke for getting an identification 
of the filter or expression.
+     * @return the previous value, or {@code null} if none.
+     * @throws IllegalArgumentException if a recursive call is detected for 
the same filter or expression.
+     */
+    private <T> Object previousResult(final T original, final Function<T, 
Object> identification) {
+        currentFilterOrExpression = original;
+        final Object previous = done.putIfAbsent(original, COMPUTING);
+        if (previous != COMPUTING) {
+            return previous;
+        }
+        throw new 
IllegalArgumentException(Errors.format(Errors.Keys.RecursiveCreateCallForKey_1, 
identification.apply(original)));
+    }
+
+    /**
+     * Stores the result of an optimization.
+     *
+     * @param original  the filter or expression to optimize.
+     * @param result    the optimization result.
+     */
+    private void storeResult(final Object original, final Object result) {
+        if (done.put(original, result) != COMPUTING) {
+            // Should not happen unless this `Optimization` is used 
concurrently in many threads.
+            throw new ConcurrentModificationException();
+        }
+    }
+
     /**
      * Optimizes or simplifies the given filter. If the given instance 
implements the {@link OnFilter} interface,
      * then its {@code optimize(this)} method is invoked. Otherwise this 
method returns the given filter as-is.
@@ -330,34 +454,29 @@ public class Optimization {
      * @throws IllegalArgumentException if the given filter is already in 
process of being optimized
      *         (i.e. there is a recursive call to {@code apply(…)} for the 
same filter).
      */
-    public <R> Filter<R> apply(final Filter<R> filter) {
-        if (!(filter instanceof OnFilter<?>)) {
-            return filter;
-        }
-        final boolean isFirstCall = (done == null);
-        if (isFirstCall) {
-            done = new IdentityHashMap<>();
-        }
-        try {
-            final Object previous = done.putIfAbsent(filter, COMPUTING);
-            if (previous == COMPUTING) {
-                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.RecursiveCreateCallForKey_1, 
filter.getOperatorType()));
-            }
-            @SuppressWarnings("unchecked")
-            Filter<R> result = (Filter<R>) previous;
-            if (result == null) {
-                result = ((OnFilter<R>) filter).optimize(this);
-                if (done.put(filter, result) != COMPUTING) {
-                    // Should not happen unless this `Optimization` is used 
concurrently in many threads.
-                    throw new ConcurrentModificationException();
+    @SuppressWarnings("unchecked")
+    public <R> Filter<R> apply(Filter<R> filter) {
+        if (filter instanceof OnFilter<?>) {
+            final var original = (OnFilter<R>) filter;
+            final boolean isFirstCall = isFirstCall();
+            final Object oldFilterOrExpression = currentFilterOrExpression;
+            try {
+                filter = (Filter<R>) previousResult(original, 
Filter::getOperatorType);
+                if (filter == null) {
+                    filter = original.optimize(this);
+                    storeResult(original, filter);
+                }
+            } finally {
+                currentFilterOrExpression = oldFilterOrExpression;
+                if (isFirstCall) {
+                    done = null;
                 }
             }
-            return result;
-        } finally {
-            if (isFirstCall) {
-                done = null;
+            if (isFirstCall && isImprovable(filter, 
Optimization::isOptimizable)) {
+                filter = DynamicOptimization.ofAny(filter);
             }
         }
+        return filter;
     }
 
     /**
@@ -368,6 +487,9 @@ public class Optimization {
      * <p>Implementations need to override only one of the 2 methods defined 
in this interface.</p>
      *
      * @param  <R>  the type of resources to filter.
+     *
+     * @version 1.6
+     * @since   1.1
      */
     public interface OnFilter<R> extends Filter<R> {
         /**
@@ -505,34 +627,29 @@ public class Optimization {
      * @throws IllegalArgumentException if the given expression is already in 
process of being optimized
      *         (i.e. there is a recursive call to {@code apply(…)} for the 
same expression).
      */
-    public <R,V> Expression<R, ? extends V> apply(final Expression<R,V> 
expression) {
-        if (!(expression instanceof OnExpression<?,?>)) {
-            return expression;
-        }
-        final boolean isFirstCall = (done == null);
-        if (isFirstCall) {
-            done = new IdentityHashMap<>();
-        }
-        try {
-            final Object previous = done.putIfAbsent(expression, COMPUTING);
-            if (previous == COMPUTING) {
-                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.RecursiveCreateCallForKey_1, 
expression.getFunctionName()));
-            }
-            @SuppressWarnings("unchecked")
-            Expression<R, ? extends V> result = (Expression<R, ? extends V>) 
previous;
-            if (result == null) {
-                result = ((OnExpression<R,V>) expression).optimize(this);
-                if (done.put(expression, result) != COMPUTING) {
-                    // Should not happen unless this `Optimization` is used 
concurrently in many threads.
-                    throw new ConcurrentModificationException();
+    @SuppressWarnings("unchecked")
+    public <R,V> Expression<R, ? extends V> apply(Expression<R, ? extends V> 
expression) {
+        if (expression instanceof OnExpression<?,?>) {
+            final var original = (OnExpression<R, ? extends V>) expression;
+            final boolean isFirstCall = isFirstCall();
+            final Object oldFilterOrExpression = currentFilterOrExpression;
+            try {
+                expression = (Expression<R, ? extends V>) 
previousResult(original, Expression::getFunctionName);
+                if (expression == null) {
+                    expression = original.optimize(this);
+                    storeResult(original, expression);
+                }
+                if (isFirstCall && isImprovable(expression, 
Optimization::isOptimizable)) {
+                    expression = DynamicOptimization.ofAny(expression);
+                }
+            } finally {
+                currentFilterOrExpression = oldFilterOrExpression;
+                if (isFirstCall) {
+                    done = null;
                 }
-            }
-            return result;
-        } finally {
-            if (isFirstCall) {
-                done = null;
             }
         }
+        return expression;
     }
 
     /**
@@ -544,6 +661,9 @@ public class Optimization {
      *
      * @param  <R>  the type of resources used as inputs.
      * @param  <V>  the type of values computed by the expression.
+     *
+     * @version 1.6
+     * @since   1.1
      */
     public interface OnExpression<R,V> extends Expression<R,V> {
         /**
@@ -672,6 +792,33 @@ public class Optimization {
         throw new IllegalArgumentException();
     }
 
+    /**
+     * Reports that a warning occurred during the execution of an {@code 
apply(…)} method.
+     * This method can be invoked by implementations of {@link OnFilter} or 
{@link OnExpression} interfaces.
+     * The exception if often a {@link PropertyNotFoundException}, which is 
not necessarily an error because
+     * a property may not exist in a base feature type but exists in some 
sub-types.
+     *
+     * <p>If the {@code resolvable} flag is {@code true}, it will be taken as 
a hint to retry the optimization
+     * later if more information about the {@link #getFinalFeatureTypes() 
final feature types} become available.
+     * This flag should be {@code false} if more information would not have a 
direct impact on the optimization
+     * done by the caller. Callers do not need to care about indirect impacts 
in the parameters of the filter or
+     * expression.</p>
+     *
+     * @param exception   the recoverable exception that occurred.
+     * @param resolvable  whether an optimization with more information may 
avoid this warning.
+     *
+     * @since 1.6
+     */
+    public void warning(Exception exception, boolean resolvable) {
+        isImprovable |= resolvable;
+        final Consumer<WarningEvent> listener = WarningEvent.LISTENER.get();
+        if (listener != null) {
+            listener.accept(new WarningEvent(currentFilterOrExpression, 
exception, true));
+        } else {
+            Logging.recoverableException(Node.LOGGER, Optimization.class, 
"apply", exception);
+        }
+    }
+
     /**
      * Returns the manner in which values are computed from resources given to 
the specified filter.
      * This set of properties may determine which optimizations are allowed.
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 b5e930790a..8120298a2a 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
@@ -269,8 +269,9 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
                     return new AsObject(preferredName, isVirtual);
                 }
             } catch (PropertyNotFoundException e) {
-                warning(e, true);
-                if (found.isEmpty()) {
+                boolean resolved = found.isEmpty();
+                optimization.warning(e, !resolved);
+                if (resolved) {
                     return NULL();      // The property does not exist in any 
feature type.
                 }
             }
@@ -346,8 +347,9 @@ abstract class PropertyValue<V> extends 
LeafExpression<Feature,V>
             try {
                 preferredName = optimization.getPreferredPropertyName(name, 
found);
             } catch (PropertyNotFoundException e) {
-                warning(e, true);
-                return found.isEmpty() ? NULL() : this;
+                boolean resolved = found.isEmpty();
+                optimization.warning(e, !resolved);
+                return resolved ? NULL() : this;
             }
             /*
              * Check if the same converter can be used for all feature types.
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunction.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunction.java
index 42b51f6771..2c14cb2542 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunction.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunction.java
@@ -157,6 +157,7 @@ public abstract class BinaryFunction<R,V1,V2> extends Node {
             /*
              * Integer overflow, or division by zero, or attempt to convert 
NaN or infinity
              * to `BigDecimal`, or division does not have a terminating 
decimal expansion.
+             * This is a recoverable because we can fallback on floating point 
arithmetic.
              */
             warning(e, true);
         }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/WarningEvent.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/WarningEvent.java
index 7476717f9e..469bb8e950 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/WarningEvent.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/WarningEvent.java
@@ -19,6 +19,7 @@ package org.apache.sis.filter.base;
 import java.util.Optional;
 import java.util.function.Consumer;
 import org.opengis.util.ScopedName;
+import org.apache.sis.util.Classes;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.util.CodeList;
@@ -44,7 +45,7 @@ public final class WarningEvent {
     /**
      * The filter or expression that produced this warning.
      */
-    private final Node source;
+    private final Object source;
 
     /**
      * The exception that occurred.
@@ -61,10 +62,11 @@ public final class WarningEvent {
     /**
      * Creates a new warning.
      *
-     * @param  source     the filter or expression that produced this warning.
-     * @param  exception  the exception that occurred.
+     * @param  source       the filter or expression that produced this 
warning.
+     * @param  exception    the exception that occurred.
+     * @param  recoverable  {@code true} for fine level, {@code false} for 
warning level.
      */
-    public WarningEvent(final Node source, final Exception exception, final 
boolean recoverable) {
+    public WarningEvent(final Object source, final Exception exception, final 
boolean recoverable) {
         this.source      = source;
         this.exception   = exception;
         this.recoverable = recoverable;
@@ -101,6 +103,7 @@ public final class WarningEvent {
      * If there is many parameter assignable to the given type, then the first 
occurrence is returned.
      * The {@code type} argument is typically {@code Literal.class} or {@code 
ValueReference.class}.
      *
+     * @param  <P>   the compile-time value of {@code type}.
      * @param  type  the desired type of the parameter to return.
      * @return the first parameter of the given type, or empty if none.
      */
@@ -130,6 +133,8 @@ public final class WarningEvent {
      */
     @Override
     public String toString() {
-        return source.getDisplayName() + ": " + exception.toString();
+        return (source instanceof Node ? ((Node) source).getDisplayName()
+                                       : Classes.getShortClassName(source))
+                + ": " + exception;
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
index d66ebf656b..667c91b096 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/sqlmm/TwoGeometries.java
@@ -85,7 +85,7 @@ class TwoGeometries<R> extends SpatialFunction<R> {
                     return recreate(effective);
                 }
             } catch (PropertyNotFoundException | TransformException e) {
-                warning(e, true);
+                optimization.warning(e, false);
             }
         }
         return super.optimize(optimization);
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 66b9911c69..41e21c6d26 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
@@ -64,7 +64,7 @@ public final class AbstractFeatureTest extends 
FeatureTestCase {
          * If a feature backed by a {@code java.util.Map} is really wanted, 
then {@link SparseFeature} should
          * be considered.
          */
-        private HashMap<String,Object> values = new HashMap<>();
+        private HashMap<String, Object> values = new HashMap<>();
 
         /**
          * Creates a new feature of the given type. This constructor adds 
immediately the default values into
@@ -145,7 +145,7 @@ public final class AbstractFeatureTest extends 
FeatureTestCase {
             } catch (CloneNotSupportedException e) {
                 throw new AssertionError(e);
             }
-            c.values = (HashMap<String,Object>) c.values.clone();
+            c.values = (HashMap<String, Object>) c.values.clone();
             return c;
         }
     }
diff --git 
a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/DynamicOptimizationTest.java
 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/DynamicOptimizationTest.java
new file mode 100644
index 0000000000..add8852fd9
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/filter/DynamicOptimizationTest.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.sis.feature.AbstractFeature;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.filter.base.Node;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.test.TestCaseWithLogs;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.feature.Feature;
+import org.opengis.filter.FilterFactory;
+
+
+/**
+ * Tests {@link DynamicOptimization} implementation.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+@SuppressWarnings("exports")
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public final class DynamicOptimizationTest extends TestCaseWithLogs {
+    /**
+     * A feature for a city with "name" and "population" attributes.
+     */
+    private final Feature city;
+
+    /**
+     * A feature for a city which is a capital.
+     */
+    private final Feature capital;
+
+    /**
+     * The factory to use for creating the objects to test.
+     */
+    private final FilterFactory<Feature, ?, ?> factory;
+
+    /**
+     * Creates a new test case.
+     */
+    public DynamicOptimizationTest() {
+        super(Node.LOGGER);
+        factory = DefaultFilterFactory.forFeatures();
+        final var builder = new FeatureTypeBuilder();
+        builder.addAttribute(String.class).setName("name");
+        builder.addAttribute(Integer.class).setName("population");
+        city = builder.setName("City").build().newInstance();
+        city.setPropertyValue("name",       "Osaka");
+        city.setPropertyValue("population", 2752024);  // In 2020 (Wikipedia).
+
+        builder.clear().setSuperTypes(city.getType());
+        builder.addAttribute(String.class).setName("country");
+        capital = builder.setName("Capital").build().newInstance();
+        capital.setPropertyValue("country",    "Japan");
+        capital.setPropertyValue("name",       "Tokyo");
+        capital.setPropertyValue("population", 14192184);  // In 2024 
(Wikipedia).
+    }
+
+    /**
+     * Tests an optimization which can be resolved in advance.
+     * We use the {@code "population"} property which exists in all feature 
types.
+     * No {@link DynamicOptimization} should be needed for this case.
+     */
+    @Test
+    public void testWithPropertyResolvedInAdvance() {
+        final var filter = factory.greater(factory.property("population", 
Integer.class), factory.literal(10000000));
+        assertFalse(filter.test(city));
+        assertTrue(filter.test(capital));
+        loggings.assertNoUnexpectedLog();
+
+        final var optimization = new Optimization();
+        optimization.setFinalFeatureTypes(Set.of(city.getType(), 
capital.getType()));
+        final var optimized = optimization.apply(filter);
+        assertFalse(optimized instanceof DynamicOptimization<?>);
+        assertNotSame(filter, optimized);   // Optimized but not dynamic.
+        assertFalse(optimized.test(city));
+        assertTrue(optimized.test(capital));
+        loggings.assertNoUnexpectedLog();
+
+        var checked = new CheckedInstance(city, "population");
+        assertFalse(optimized.test(checked));
+        checked.assertAllPropertiesHaveBeenRead();
+
+        checked = new CheckedInstance(capital, "population");
+        assertTrue(optimized.test(checked));
+        checked.assertAllPropertiesHaveBeenRead();
+    }
+
+    /**
+     * Tests an optimization which cannot be resolved in advance.
+     * A {@link DynamicOptimization} is needed for this case.
+     */
+    @Test
+    public void testDynamic() {
+        final var filter = factory.equal(factory.property("country", 
String.class), factory.literal("Japan"));
+        assertFalse(filter.test(city));
+        loggings.assertNextLogContains("country", "City");
+        assertTrue(filter.test(capital));
+        loggings.assertNoUnexpectedLog();
+
+        final var optimization = new Optimization();
+        optimization.setFinalFeatureTypes(Set.of(city.getType(), 
capital.getType()));
+        final var optimized = optimization.apply(filter);
+        assertInstanceOf(DynamicOptimization.class, optimized);
+        assertFalse(optimized.test(city));
+        assertTrue(optimized.test(capital));
+        loggings.assertNoUnexpectedLog();
+
+        var checked = new CheckedInstance(city);
+        assertFalse(optimized.test(checked));
+        checked.assertAllPropertiesHaveBeenRead();
+        loggings.assertNoUnexpectedLog();
+
+        checked = new CheckedInstance(capital, "country");
+        assertTrue(optimized.test(checked));
+        checked.assertAllPropertiesHaveBeenRead();
+        loggings.assertNoUnexpectedLog();
+    }
+
+    /**
+     * A feature instance which ensure that no unexpected method is invoked.
+     */
+    @SuppressWarnings("serial")
+    private static final class CheckedInstance extends AbstractFeature {
+        /** The original feature to wrap. */
+        private final Feature original;
+
+        /** Names of the properties that are expected to be queried. */
+        private final Set<String> expectedQueries;
+
+        /** Creates a wrapper for the given feature instance. */
+        CheckedInstance(final Feature original, final String... expected) {
+            super(original.getType());
+            this.original = original;
+            expectedQueries = new HashSet<>(Arrays.asList(expected));
+        }
+
+        /** Returns the value of the property of the given name. */
+        @Override public Object getPropertyValue(final String name) {
+            assertTrue(expectedQueries.remove(name), name);
+            return original.getPropertyValue(name);
+        }
+
+        /** Should not be invoked. */
+        @Override public void setPropertyValue(String name, Object value) {
+            fail();
+        }
+
+        /** Verifies that all properties that we expected to be read have been 
read. */
+        void assertAllPropertiesHaveBeenRead() {
+            assertTrue(expectedQueries.isEmpty(), expectedQueries.toString());
+        }
+    }
+}
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 8106553156..e1bae3fb01 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
@@ -246,8 +246,8 @@ public final class LogicalFilterTest extends TestCase {
          * Prepare an expression which divide the population value by 5.
          */
         final var expression = factory.divide(factory.property(attribute, 
Integer.class), factory.literal(5));
-        final Optimization optimization = new Optimization();
-        assertSame(expression, optimization.apply(expression));                
       // No optimization.
+        final var optimization = new Optimization();
+        assertInstanceOf(DynamicOptimization.class, 
optimization.apply(expression));
         assertEquals(200, expression.apply(instance).intValue());
         /*
          * Notify the optimizer that property values will be of `String` type.

Reply via email to