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 c6a2901fddda0682003e457abbe6021691c64747
Merge: da1cc1c2df bd9026738e
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Thu Mar 2 16:24:43 2023 +0100

    Merge remote-tracking branch 'origin/feat/filtercopy' into geoapi-4.0.
    Modifications to the pull request:
    - Add the missing parameterized types.
    - Register action for each filter type using the mechanism provided by 
`Visitor` parent class.
    - Reuse previously existing filter or expression instances when possible.

 .../apache/sis/internal/filter/CopyVisitor.java    | 712 +++++++++++++++++++++
 .../apache/sis/internal/filter/EditVisitor.java    |  56 ++
 .../apache/sis/internal/filter/package-info.java   |   2 +-
 .../sis/internal/filter/CopyVisitorTest.java       |  77 +++
 .../sis/internal/filter/FilterFactoryMock.java     | 562 ++++++++++++++++
 .../apache/sis/internal/filter/FunctionMock.java   |  91 +++
 .../sis/internal/filter/ValueReferenceMock.java    |  82 +++
 .../apache/sis/test/suite/FeatureTestSuite.java    |   3 +-
 8 files changed, 1583 insertions(+), 2 deletions(-)

diff --cc 
core/sis-feature/src/main/java/org/apache/sis/internal/filter/CopyVisitor.java
index 0000000000,a3847de3c6..a02cf00328
mode 000000,100644..100644
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/CopyVisitor.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/CopyVisitor.java
@@@ -1,0 -1,232 +1,712 @@@
+ /*
+  * 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.internal.filter;
+ 
 -import java.util.ArrayList;
+ import java.util.List;
 -import java.util.concurrent.atomic.AtomicReference;
 -import org.apache.sis.util.ArgumentChecks;
 -import org.opengis.filter.BetweenComparisonOperator;
 -import org.opengis.filter.BinaryComparisonOperator;
 -import org.opengis.filter.DistanceOperator;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.FilterFactory;
 -import org.opengis.filter.LikeOperator;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.LogicalOperator;
 -import org.opengis.filter.MatchAction;
 -import org.opengis.filter.NilOperator;
 -import org.opengis.filter.NullOperator;
 -import org.opengis.filter.ValueReference;
 -import org.opengis.geometry.Envelope;
++import java.util.ArrayList;
++import java.util.Collection;
++import javax.measure.Quantity;
++import javax.measure.quantity.Length;
+ import org.opengis.util.CodeList;
++import org.opengis.geometry.Envelope;
++import org.apache.sis.util.ArgumentChecks;
++import org.apache.sis.util.resources.Errors;
++import org.apache.sis.internal.feature.Resources;
++import org.apache.sis.internal.util.CollectionsExt;
++
++// Branch-dependent imports
++import org.opengis.filter.*;
++
+ 
+ /**
 - * Visitor used to copy expressions and filters from one factory to another.
 - * This class purpose is to offer a way to convert filters to different and
 - * often more specialized and efficient implementations such as for Coverages 
or SQL.
++ * Visitor used to copy expressions and filters with potentially a change of 
parameterized types.
++ * This class can be used when filters need to be recreated using a different 
{@link FilterFactory},
++ * for example because the type of resources changed. For example different 
filter implementations
++ * may be needed if the filters need to operate on {@link 
org.apache.sis.coverage.grid.GridCoverage}
++ * resources instead of {@link org.opengis.feature.Feature} instances.
+  *
 - * @author Johann Sorel (Geomatys)
++ * @author  Johann Sorel (Geomatys)
++ * @author  Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ *
++ * @param  <SR>  the type of resources expected by the filters to copy 
(source resource).
++ * @param  <TR>  the type of resources expected by the copied filters (target 
resource).
++ * @param  <G>   base class of geometry objects.
++ * @param  <T>   base class of temporal objects.
++ *
++ * @since 1.4
+  */
 -public final class CopyVisitor<R,T> extends Visitor<R,AtomicReference> {
++public class CopyVisitor<SR,TR,G,T> extends Visitor<SR, List<Object>> {
++    /**
++     * The factory to use for creating the new filters and expressions.
++     * Note that some methods in this factory may return {@code null}.
++     * See <i>Partially implemented factory</i> in {@link EditVisitor} 
Javadoc.
++     */
++    protected final FilterFactory<TR,G,T> factory;
+ 
 -    private final FilterFactory<T,Object,Object> targetFactory;
++    /**
++     * Whether to force creation of new filters or expressions even when the 
operands did not changed.
++     * If {@code false}, new filters and expressions are created only when at 
least one operand changed.
++     * This flag should be {@code true} if the {@code <SR>} and {@code <TR>} 
types are not the same,
++     * or if the user knows that the {@linkplain #factory} may create a 
different kind of object even
++     * when the operands are the same.
++     */
++    private final boolean forceNew;
+ 
+     /**
 -     * Create a new copy visitor with given factory.
++     * Whether to force the use of newly created filters or expressions even 
when they are equal to the original ones.
++     * If {@code false}, then the previously existing filters or expressions 
will be reused when the newly created
++     * instances are equal according to {@link Object#equals(Object)}. Note 
this sharing requires that the filter
++     * or expression to reuse expects a base resource type {@code <R>} which 
is common to both {@code <? super SR>}
++     * and {@code <? super TR>}. We have no way to verify that.
+      *
 -     * @param targetFactory not null
++     * <p>This flag should be {@code true} if the filters or expressions are 
mutable.
++     * Otherwise it may be {@code false} as a way to share existing 
instances.</p>
+      */
 -    public CopyVisitor(FilterFactory<T,?,?> targetFactory) {
 -        ArgumentChecks.ensureNonNull("factory", targetFactory);
 -        this.targetFactory = (FilterFactory<T, Object, Object>) targetFactory;
 -        setBinaryComparisonHandlers(this::copyBinaryComparison);
 -        setBinaryTemporalHandlers(this::copyBinaryTemporal);
 -        setLogicalHandlers(this::copyLogical);
 -        setSpatialHandlers(this::copySpatial);
 -        setMathHandlers(this::copyMath);
 -    }
++    private final boolean forceUse;
+ 
+     /**
 -     * Copy given filter using given factory.
++     * The factory method to invoke for creating a binary comparison operator.
++     * This is used for invoking one of the {@link FilterFactory} methods for
++     * a given {@link ComparisonOperatorName}. Example:
++     *
++     * {@snippet lang="java" :
++     *     
copyVisitor.setCopyHandler(ComparisonOperatorName.PROPERTY_IS_EQUAL_TO, 
FilterFactory::equal);
++     * }
+      *
 -     * @param filter filter to copy
 -     * @return copied filter.
++     * @param  <R>  the same type of resources specified by the enclosing 
{@link CopyVisitor} class.
++     *
++     * @see CopyVisitor#setCopyHandler(ComparisonOperatorName, 
BinaryComparisonFactory)
+      */
 -    public Filter<T> copy(Filter<R> filter) {
 -        final AtomicReference ref = new AtomicReference();
 -        visit(filter, ref);
 -        return (Filter) ref.get();
++    @FunctionalInterface
++    protected interface BinaryComparisonFactory<R> {
++        /**
++         * Creates a new binary comparison operator from the given operands.
++         *
++         * @param  factory         the factory to use for creating the filter
++         * @param  expression1     the first of the two expressions to be 
used by this comparator.
++         * @param  expression2     the second of the two expressions to be 
used by this comparator.
++         * @param  isMatchingCase  specifies whether comparisons are case 
sensitive.
++         * @param  matchAction     specifies how the comparisons shall be 
evaluated for a collection of values.
++         * @return the new filter, or {@code null} if this method cannot 
create a filter from the given arguments.
++         */
++        BinaryComparisonOperator<R> create(
++                FilterFactory<R,?,?> factory,
++                Expression<? super R, ?> expression1,
++                Expression<? super R, ?> expression2,
++                boolean isMatchingCase, MatchAction matchAction);
+     }
+ 
+     /**
 -     * Copy given expression using given factory.
++     * The factory method to invoke for creating a temporal operator.
++     * This is used for invoking one of the {@link FilterFactory} methods
++     * for a given {@link TemporalOperatorName}. Example:
++     *
++     * {@snippet lang="java" :
++     *     copyVisitor.setCopyHandler(TemporalOperatorName.AFTER, 
FilterFactory::after);
++     * }
+      *
 -     * @param expression expression to copy
 -     * @return copied expression.
++     * @param  <R>  the same type of resources specified by the enclosing 
{@link CopyVisitor} class.
++     * @param  <T>  base class of temporal objects specified by the enclosing 
class.
++     *
++     * @see CopyVisitor#setCopyHandler(TemporalOperatorName, 
TemporalComparisonFactory)
+      */
 -    public Expression<T,Object> copy(Expression expression) {
 -        final AtomicReference<Expression<T, Object>> ref = new 
AtomicReference();
 -        visit(expression, ref);
 -        return ref.get();
++    @FunctionalInterface
++    protected interface TemporalFactory<R,T> {
++        /**
++         * Creates a new temporal operator from the given operands.
++         *
++         * @param  factory  the factory to use for creating the filter.
++         * @param  time1    expression fetching the first temporal value.
++         * @param  time2    expression fetching the second temporal value.
++         * @return the new filter, or {@code null} if this method cannot 
create a filter from the given arguments.
++         */
++        TemporalOperator<R> create(
++                FilterFactory<R,?,T> factory,
++                Expression<? super R, ? extends T> time1,
++                Expression<? super R, ? extends T> time2);
+     }
+ 
 -    private List<Expression<T,Object>> copyExpressions(List<Expression<? 
super R,?>> source) {
 -        final List<Expression<T,Object>> results = new 
ArrayList<>(source.size());
 -        for (Expression e : source) {
 -            results.add(copy(e));
++    /**
++     * The factory method to invoke for creating a spatial operator.
++     * This is used for invoking one of the {@link FilterFactory} methods
++     * for a given {@link SpatialOperatorName}. Example:
++     *
++     * {@snippet lang="java" :
++     *     copyVisitor.setCopyHandler(SpatialOperatorName.EQUALS, 
FilterFactory::equals);
++     * }
++     *
++     * @param  <R>  the same type of resources specified by the enclosing 
{@link CopyVisitor} class.
++     * @param  <G>  base class of geometry objects specified by the enclosing 
class.
++     *
++     * @see CopyVisitor#setCopyHandler(SpatialOperatorName, SpatialFactory)
++     */
++    @FunctionalInterface
++    protected interface SpatialFactory<R,G> {
++        /**
++         * Creates a new spatial operator from the given operands.
++         *
++         * @param  factory    the factory to use for creating the filter.
++         * @param  geometry1  expression fetching the first geometry of the 
binary operator.
++         * @param  geometry2  expression fetching the second geometry of the 
binary operator.
++         * @return the new filter, or {@code null} if this method cannot 
create a filter from the given arguments.
++         */
++        BinarySpatialOperator<R> create(
++                FilterFactory<R,G,?> factory,
++                Expression<? super R, ? extends G> geometry1,
++                Expression<? super R, ? extends G> geometry2);
++
++        /**
++         * Bridge for the "bbox" operation, because it uses a different 
method signature.
++         * Usage example:
++         *
++         * {@snippet lang="java" :
++         *     copyVisitor.setCopyHandler(SpatialOperatorName.BBOX, 
SpatialFactory::bbox);
++         * }
++         *
++         * @param  <R>        the {@link CopyVisitor} type of resources.
++         * @param  <G>        base class of geometry objects specified by 
{@code CopyVisitor}.
++         * @param  factory    the factory to use for creating the filter.
++         * @param  geometry1  expression fetching the geometry to check for 
interaction with bounds.
++         * @param  geometry2  the bounds to check geometry against.
++         * @return the new filter, or {@code null} if this method cannot 
create a filter from the given arguments.
++         *
++         * @see SpatialOperatorName#BBOX
++         */
++        static <R,G> BinarySpatialOperator<R> bbox(
++                final FilterFactory<R,G,?> factory,
++                final Expression<? super R, ? extends G> geometry1,
++                final Expression<? super R, ? extends G> geometry2)
++        {
++            if (geometry2 instanceof Literal<?,?>) {
++                final Object bounds = ((Literal<?,?>) geometry2).getValue();
++                if (bounds instanceof Envelope) {
++                    return factory.bbox(geometry1, (Envelope) bounds);
++                }
++            }
++            return null;
+         }
 -        return results;
+     }
+ 
 -    private List<Filter<T>> copyOperands(List<Filter<R>> source) {
 -        final List<Filter<T>> results = new ArrayList<>(source.size());
 -        for (Filter<R> e : source) {
 -            results.add(copy(e));
++    /**
++     * The factory method to invoke for creating a distance operator.
++     * This is used for invoking one of the {@link FilterFactory} methods
++     * for a given {@link DistanceOperatorName}. Example:
++     *
++     * {@snippet lang="java" :
++     *     copyVisitor.setCopyHandler(DistanceOperatorName.BEYOND, 
FilterFactory::beyond);
++     * }
++     *
++     * @param  <R>  the same type of resources specified by the enclosing 
{@link CopyVisitor} class.
++     * @param  <G>  base class of geometry objects specified by the enclosing 
class.
++     *
++     * @see CopyVisitor#setCopyHandler(DistanceOperatorName, DistanceFactory)
++     */
++    @FunctionalInterface
++    protected interface DistanceFactory<R,G> {
++        /**
++         * Creates a new spatial operator from the given operands.
++         *
++         * @param  factory    the factory to use for creating the filter.
++         * @param  geometry1  expression fetching the first geometry of the 
binary operator.
++         * @param  geometry2  expression fetching the second geometry of the 
binary operator.
++         * @param  distance   distance for evaluating the expression as 
{@code true}.
++         * @return the new filter, or {@code null} if this method cannot 
create a filter from the given arguments.
++         */
++        DistanceOperator<R> create(
++                FilterFactory<R,G,?> factory,
++                Expression<? super R, ? extends G> geometry1,
++                Expression<? super R, ? extends G> geometry2,
++                Quantity<Length> distance);
++    }
++
++    /**
++     * The factory method to invoke for creating a logical operator.
++     * This is used for invoking one of the {@link FilterFactory}
++     * methods for a given {@link LogicalOperatorName}. Example:
++     *
++     * {@snippet lang="java" :
++     *     copyVisitor.setCopyHandler(LogicalOperatorName.AND, 
FilterFactory::and);
++     * }
++     *
++     * @param  <R>  the same type of resources specified by the enclosing 
{@link CopyVisitor} class.
++     *
++     * @see CopyVisitor#setCopyHandler(LogicalOperatorName, LogicalFactory)
++     */
++    @FunctionalInterface
++    protected interface LogicalFactory<R> {
++        /**
++         * Creates a new binary comparison operator from the given operands.
++         *
++         * @param  factory   the factory to use for creating the filter.
++         * @param  operands  a collection of operands.
++         * @return the new filter, or {@code null} if this method cannot 
create a filter from the given arguments.
++         */
++        LogicalOperator<R> create(
++                FilterFactory<R,?,?> factory,
++                Collection<? extends Filter<? super R>> operands);
++
++        /**
++         * Bridge for the "not" operation, because it uses a different method 
signature.
++         * This method signature shall be identical to {@code create(…)} 
method signature.
++         * Usage example:
++         *
++         * {@snippet lang="java" :
++         *     copyVisitor.setCopyHandler(LogicalOperatorName.NOT, 
LogicalFactory::not);
++         * }
++         *
++         * @param  <R>       the {@link CopyVisitor} type of resources.
++         * @param  factory   the factory to use for creating the filter.
++         * @param  operands  the operand of the NOT operation.
++         * @return a filter evaluating {@code NOT operand}.
++         */
++        static <R> LogicalOperator<R> not(
++                final FilterFactory<R,?,?> factory,
++                final Collection<? extends Filter<? super R>> operands)
++        {
++            Filter<? super R> op = CollectionsExt.singletonOrNull(operands);
++            return (op != null) ? factory.not(op) : null;
+         }
 -        return results;
 -    }
 -
 -    private void copyBinaryComparison(Filter<R> t, AtomicReference u) {
 -        final BinaryComparisonOperator co = (BinaryComparisonOperator) t;
 -        final List<Expression<T, Object>> exps = 
copyExpressions(t.getExpressions());
 -        final MatchAction ma = co.getMatchAction();
 -        final boolean mc = co.isMatchingCase();
 -        final Filter<T> result;
 -        switch (t.getOperatorType().identifier()) {
 -            case "PropertyIsEqualTo" :              result = 
targetFactory.equal(exps.get(0), exps.get(1), mc, ma); break;
 -            case "PropertyIsNotEqualTo" :           result = 
targetFactory.notEqual(exps.get(0), exps.get(1), mc, ma); break;
 -            case "PropertyIsLessThan" :             result = 
targetFactory.less(exps.get(0), exps.get(1), mc, ma); break;
 -            case "PropertyIsGreaterThan" :          result = 
targetFactory.greater(exps.get(0), exps.get(1), mc, ma); break;
 -            case "PropertyIsLessThanOrEqualTo" :    result = 
targetFactory.lessOrEqual(exps.get(0), exps.get(1), mc, ma); break;
 -            case "PropertyIsGreaterThanOrEqualTo" : result = 
targetFactory.greaterOrEqual(exps.get(0), exps.get(1), mc, ma); break;
 -            default : throw new IllegalArgumentException("Unknowned filter 
type " + t.getOperatorType().identifier());
++    }
++
++    /**
++     * The factory method to invoke for creating a binary function.
++     * This is used for invoking one of the {@link FilterFactory}
++     * methods for a given arithmetic function name. Example:
++     *
++     * {@snippet lang="java" :
++     *     copyVisitor.setCopyHandler("Multiply", FilterFactory::multiply);
++     * }
++     *
++     * @param  <R>  the same type of resources specified by the enclosing 
{@link CopyVisitor} class.
++     *
++     * @see CopyVisitor#setCopyHandler(String, BinaryFunctionFactory)
++     */
++    @FunctionalInterface
++    protected interface BinaryFunctionFactory<R> {
++        /**
++         * Creates a new binary function from the given operands.
++         *
++         * @param  factory   the factory to use for creating the filter.
++         * @param  operand1  the first of the two expressions to be used by 
this function.
++         * @param  operand2  the second of the two expressions to be used by 
this function.
++         * @return the new expression, or {@code null} if this method cannot 
create an expression from the given arguments.
++         */
++        Expression<R,Number> create(
++                FilterFactory<R,?,?> factory,
++                Expression<? super R, ? extends Number> operand1,
++                Expression<? super R, ? extends Number> operand2);
++    }
++
++    /**
++     * Creates a new copy visitor with the given factory.
++     *
++     * @param  factory  the factory to use for creating the new filters and 
expressions.
++     * @param  force    whether to force new filters or expressions even when 
existing instances could be reused.
++     */
++    public CopyVisitor(final FilterFactory<TR,G,T> factory, final boolean 
force) {
++        this(factory, true, force);
++    }
++
++    /**
++     * Creates a new copy visitor with the given factory.
++     * The {@code forceNew} argument can be {@code false}
++     * only if {@code <SR>} and {@code <TR>} are the same type.
++     *
++     * @param  newFactory  the factory to use for creating the new filters 
and expressions.
++     * @param  forceNew    whether to force creation of new filters or 
expressions even when the operands did not changed.
++     * @param  forceUse    whether to force the use of newly created filters 
or expressions even when they are equal to the original ones.
++     */
++    CopyVisitor(final FilterFactory<TR,G,T> newFactory, final boolean 
forceNew, final boolean forceUse) {
++        ArgumentChecks.ensureNonNull("factory", newFactory);
++        this.factory  = newFactory;
++        this.forceNew = forceNew;
++        this.forceUse = forceUse;
++        setCopyHandler(ComparisonOperatorName.PROPERTY_IS_EQUAL_TO,           
      FilterFactory::equal);
++        setCopyHandler(ComparisonOperatorName.PROPERTY_IS_NOT_EQUAL_TO,       
      FilterFactory::notEqual);
++        setCopyHandler(ComparisonOperatorName.PROPERTY_IS_LESS_THAN,          
      FilterFactory::less);
++        setCopyHandler(ComparisonOperatorName.PROPERTY_IS_GREATER_THAN,       
      FilterFactory::greater);
++        
setCopyHandler(ComparisonOperatorName.PROPERTY_IS_LESS_THAN_OR_EQUAL_TO,    
FilterFactory::lessOrEqual);
++        
setCopyHandler(ComparisonOperatorName.PROPERTY_IS_GREATER_THAN_OR_EQUAL_TO, 
FilterFactory::greaterOrEqual);
++        setCopyHandler(  TemporalOperatorName.AFTER,                          
      FilterFactory::after);
++        setCopyHandler(  TemporalOperatorName.BEFORE,                         
      FilterFactory::before);
++        setCopyHandler(  TemporalOperatorName.BEGINS,                         
      FilterFactory::begins);
++        setCopyHandler(  TemporalOperatorName.BEGUN_BY,                       
      FilterFactory::begunBy);
++        setCopyHandler(  TemporalOperatorName.CONTAINS,                       
      FilterFactory::tcontains);
++        setCopyHandler(  TemporalOperatorName.DURING,                         
      FilterFactory::during);
++        setCopyHandler(  TemporalOperatorName.EQUALS,                         
      FilterFactory::tequals);
++        setCopyHandler(  TemporalOperatorName.OVERLAPS,                       
      FilterFactory::toverlaps);
++        setCopyHandler(  TemporalOperatorName.MEETS,                          
      FilterFactory::meets);
++        setCopyHandler(  TemporalOperatorName.ENDS,                           
      FilterFactory::ends);
++        setCopyHandler(  TemporalOperatorName.OVERLAPPED_BY,                  
      FilterFactory::overlappedBy);
++        setCopyHandler(  TemporalOperatorName.MET_BY,                         
      FilterFactory::metBy);
++        setCopyHandler(  TemporalOperatorName.ENDED_BY,                       
      FilterFactory::endedBy);
++        setCopyHandler(  TemporalOperatorName.ANY_INTERACTS,                  
      FilterFactory::anyInteracts);
++        setCopyHandler(   SpatialOperatorName.BBOX,                           
     SpatialFactory::bbox);
++        setCopyHandler(   SpatialOperatorName.EQUALS,                         
      FilterFactory::equals);
++        setCopyHandler(   SpatialOperatorName.DISJOINT,                       
      FilterFactory::disjoint);
++        setCopyHandler(   SpatialOperatorName.INTERSECTS,                     
      FilterFactory::intersects);
++        setCopyHandler(   SpatialOperatorName.TOUCHES,                        
      FilterFactory::touches);
++        setCopyHandler(   SpatialOperatorName.CROSSES,                        
      FilterFactory::crosses);
++        setCopyHandler(   SpatialOperatorName.WITHIN,                         
      FilterFactory::within);
++        setCopyHandler(   SpatialOperatorName.CONTAINS,                       
      FilterFactory::contains);
++        setCopyHandler(   SpatialOperatorName.OVERLAPS,                       
      FilterFactory::overlaps);
++        setCopyHandler(  DistanceOperatorName.WITHIN,                         
      FilterFactory::within);
++        setCopyHandler(  DistanceOperatorName.BEYOND,                         
      FilterFactory::beyond);
++        setCopyHandler(   LogicalOperatorName.AND,                            
      FilterFactory::and);
++        setCopyHandler(   LogicalOperatorName.OR,                             
      FilterFactory::or);
++        setCopyHandler(   LogicalOperatorName.NOT,                            
     LogicalFactory::not);
++        setCopyHandler(         FunctionNames.Add,                            
      FilterFactory::add);
++        setCopyHandler(         FunctionNames.Subtract,                       
      FilterFactory::subtract);
++        setCopyHandler(         FunctionNames.Multiply,                       
      FilterFactory::multiply);
++        setCopyHandler(         FunctionNames.Divide,                         
      FilterFactory::divide);
++        /*
++         * Following are factory methods with different signatures, but where 
each signature appears only once.
++         * It is not worth to create e.g. a `TrinaryComparisonFactory` 
functional interface for only one method.
++         */
++        
setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_BETWEEN),
 (filter, accumulator) -> {
++            BetweenComparisonOperator<TR> target = null;
++            BetweenComparisonOperator<SR> source = 
(BetweenComparisonOperator<SR>) filter;
++            var exps = copyExpressions(source.getExpressions());
++            if (exps != null && exps.size() == 2) {
++                target = factory.between(exps.get(0), exps.get(1), 
exps.get(2));
++            }
++            accept(accumulator, source, target);
++        });
++        
setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_LIKE),
 (filter, accumulator) -> {
++            LikeOperator<TR> target = null;
++            LikeOperator<SR> source = (LikeOperator<SR>) filter;
++            var exps = copyExpressions(source.getExpressions());
++            if (exps != null && exps.size() == 2) {
++                final Expression<?,?> p2 = exps.get(1);
++                if (p2 instanceof Literal<?,?>) {
++                    final Object literal = ((Literal<?,?>) p2).getValue();
++                    if (literal instanceof String) {
++                        target = factory.like(exps.get(0), (String) literal, 
source.getWildCard(),
++                                source.getSingleChar(), 
source.getEscapeChar(), source.isMatchingCase());
++                    }
++                }
++            }
++            accept(accumulator, source, target);
++        });
++        
setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_NIL), 
(filter, accumulator) -> {
++            NilOperator<TR> target = null;
++            NilOperator<SR> source = (NilOperator<SR>) filter;
++            var exps = copyExpressions(source.getExpressions());
++            if (exps != null && exps.size() == 1) {
++                target = factory.isNil(exps.get(0), 
source.getNilReason().orElse(null));
++            }
++            accept(accumulator, source, target);
++        });
++        
setFilterHandler(ComparisonOperatorName.valueOf(FunctionNames.PROPERTY_IS_NULL),
 (filter, accumulator) -> {
++            NullOperator<TR> target = null;
++            NullOperator<SR> source = (NullOperator<SR>) filter;
++            var exps = copyExpressions(source.getExpressions());
++            if (exps != null && exps.size() == 1) {
++                target = factory.isNull(exps.get(0));
++            }
++            accept(accumulator, source, target);
++        });
++        setExpressionHandler(FunctionNames.Literal, (expression, accumulator) 
-> {
++            Literal<SR,?> source = (Literal<SR,?>) expression;
++            Literal<TR,?> target = factory.literal(source.getValue());
++            accept(accumulator, source, target);
++        });
++        setExpressionHandler(FunctionNames.ValueReference, (expression, 
accumulator) -> {
++            ValueReference<SR,?> source = (ValueReference<SR,?>) expression;
++            ValueReference<TR,?> target = factory.property(source.getXPath());
++            accept(accumulator, source, target);
++        });
++    }
++
++    /**
++     * Sets the action to execute for the given type of binary comparison 
operator.
++     * Example:
++     *
++     * {@snippet lang="java" :
++     *     setCopyHandler(ComparisonOperatorName.PROPERTY_IS_EQUAL_TO, 
FilterFactory::equal);
++     * }
++     *
++     * @param  type    identification of the filter type.
++     * @param  action  the action to execute when the identified filter is 
found.
++     */
++    protected final void setCopyHandler(final ComparisonOperatorName type, 
final BinaryComparisonFactory<TR> action) {
++        setFilterHandler(type, (filter, accumulator) -> {
++            BinaryComparisonOperator<TR> target = null;
++            BinaryComparisonOperator<SR> source = 
(BinaryComparisonOperator<SR>) filter;
++            var exps = copyExpressions(source.getExpressions());
++            if (exps != null && exps.size() == 2) {
++                target = action.create(factory, exps.get(0), exps.get(1), 
source.isMatchingCase(), source.getMatchAction());
++            }
++            accept(accumulator, source, target);
++        });
++    }
++
++    /**
++     * Sets the action to execute for the given type of temporal operator.
++     * Example:
++     *
++     * {@snippet lang="java" :
++     *     setCopyHandler(TemporalOperatorName.AFTER, FilterFactory::after);
++     * }
++     *
++     * @param  type    identification of the filter type.
++     * @param  action  the action to execute when the identified filter is 
found.
++     */
++    protected final void setCopyHandler(final TemporalOperatorName type, 
final TemporalFactory<TR,T> action) {
++        setFilterHandler(type, (filter, accumulator) -> {
++            TemporalOperator<TR> target = null;
++            TemporalOperator<SR> source = (TemporalOperator<SR>) filter;
++            List<Expression<TR,T>> exps = 
copyExpressions(source.getExpressions());
++            if (exps != null && exps.size() == 2) {
++                target = action.create(factory, exps.get(0), exps.get(1));
++            }
++            accept(accumulator, source, target);
++        });
++    }
++
++    /**
++     * Sets the action to execute for the given type of spatial operator.
++     * Example:
++     *
++     * {@snippet lang="java" :
++     *     setCopyHandler(SpatialOperatorName.EQUALS, FilterFactory::equals);
++     * }
++     *
++     * @param  type    identification of the filter type.
++     * @param  action  the action to execute when the identified filter is 
found.
++     */
++    protected final void setCopyHandler(final SpatialOperatorName type, final 
SpatialFactory<TR,G> action) {
++        setFilterHandler(type, (filter, accumulator) -> {
++            BinarySpatialOperator<TR> target = null;
++            BinarySpatialOperator<SR> source = (BinarySpatialOperator<SR>) 
filter;
++            List<Expression<TR,G>> exps = 
copyExpressions(source.getExpressions());
++            if (exps != null && exps.size() == 2) {
++                target = action.create(factory, exps.get(0), exps.get(1));
++            }
++            accept(accumulator, source, target);
++        });
++    }
++
++    /**
++     * Sets the action to execute for the given type of distance operator.
++     * Example:
++     *
++     * {@snippet lang="java" :
++     *     setCopyHandler(DistanceOperatorName.BEYOND, FilterFactory::beyond);
++     * }
++     *
++     * @param  type    identification of the filter type.
++     * @param  action  the action to execute when the identified filter is 
found.
++     */
++    protected final void setCopyHandler(final DistanceOperatorName type, 
final DistanceFactory<TR,G> action) {
++        setFilterHandler(type, (filter, accumulator) -> {
++            DistanceOperator<TR> target = null;
++            DistanceOperator<SR> source = (DistanceOperator<SR>) filter;
++            List<Expression<TR,G>> exps = 
copyExpressions(source.getExpressions());
++            if (exps != null && exps.size() == 3) {
++                target = action.create(factory, exps.get(0), exps.get(1), 
source.getDistance());
++            }
++            accept(accumulator, source, target);
++        });
++    }
++
++    /**
++     * Sets the action to execute for the given type of logical operator.
++     * Example:
++     *
++     * {@snippet lang="java" :
++     *     setCopyHandler(LogicalOperatorName.AND, FilterFactory::and);
++     * }
++     *
++     * @param  type    identification of the filter type.
++     * @param  action  the action to execute when the identified filter is 
found.
++     */
++    protected final void setCopyHandler(final LogicalOperatorName type, final 
LogicalFactory<TR> action) {
++        setFilterHandler(type, (filter, accumulator) -> {
++            LogicalOperator<TR> target = null;
++            LogicalOperator<SR> source = (LogicalOperator<SR>) filter;
++            final var exps = copyFilters(source.getOperands());
++            if (exps != null) {
++                target = action.create(factory, exps);
++            }
++            accept(accumulator, source, target);
++        });
++    }
++
++    /**
++     * Sets the action to execute for the given function.
++     * Example:
++     *
++     * {@snippet lang="java" :
++     *     setCopyHandler("Multiply", FilterFactory::multiply);
++     * }
++     *
++     * @param  name    identification of the function.
++     * @param  action  the action to execute when the identified function is 
found.
++     */
++    protected final void setCopyHandler(final String name, final 
BinaryFunctionFactory<TR> action) {
++        setExpressionHandler(name, (source, accumulator) -> {
++            Expression<TR,?> target = null;
++            List<Expression<TR,Number>> exps = 
copyExpressions(source.getParameters());
++            if (exps != null) {
++                target = action.create(factory, exps.get(0), exps.get(1));
++            }
++            accept(accumulator, source, target);
++        });
++    }
++
++    /**
++     * Adds the target filter or expression in the list of copied elements.
++     *
++     * @param  accumulator  the list of copied elements where to add the 
{@code target} element.
++     * @param  source       the original filter or expression which has been 
copied.
++     * @param  target       the copied filter or expression, or {@code null} 
if the copy could not be done.
++     * @throws IllegalArgumentException if {@code source} copy was mandated 
and couldn't be done.
++     */
++    private void accept(final List<Object> accumulator, final Object source, 
Object target) {
++        if (target == null) {
++            if (forceNew) {
++                throw new 
IllegalArgumentException(Errors.format(Errors.Keys.CanNotCopy_1, source));
++            }
++            target = source;
++        } else if (!forceUse && target.equals(source)) {
++            target = source;
+         }
 -        u.set(result);
 -    }
 -
 -    private void copyBinaryTemporal(Filter<R> t, AtomicReference u) {
 -        final List<Expression<T, Object>> exps = 
copyExpressions(t.getExpressions());
 -        final Filter<T> result;
 -        switch (t.getOperatorType().identifier()) {
 -            case "After" :          result = targetFactory.after(exps.get(0), 
exps.get(1)); break;
 -            case "Before" :         result = 
targetFactory.before(exps.get(0), exps.get(1)); break;
 -            case "Begins" :         result = 
targetFactory.begins(exps.get(0), exps.get(1)); break;
 -            case "BegunBy" :        result = 
targetFactory.begunBy(exps.get(0), exps.get(1)); break;
 -            case "TContains" :      result = 
targetFactory.tcontains(exps.get(0), exps.get(1)); break;
 -            case "During" :         result = 
targetFactory.during(exps.get(0), exps.get(1)); break;
 -            case "TEquals" :        result = 
targetFactory.tequals(exps.get(0), exps.get(1)); break;
 -            case "TOverlaps" :      result = 
targetFactory.toverlaps(exps.get(0), exps.get(1)); break;
 -            case "Meets" :          result = targetFactory.meets(exps.get(0), 
exps.get(1)); break;
 -            case "Ends" :           result = targetFactory.ends(exps.get(0), 
exps.get(1)); break;
 -            case "OverlappedBy" :   result = 
targetFactory.overlappedBy(exps.get(0), exps.get(1)); break;
 -            case "MetBy" :          result = targetFactory.metBy(exps.get(0), 
exps.get(1)); break;
 -            case "EndedBy" :        result = 
targetFactory.endedBy(exps.get(0), exps.get(1)); break;
 -            case "AnyInteracts" :   result = 
targetFactory.anyInteracts(exps.get(0), exps.get(1)); break;
 -            default : throw new IllegalArgumentException("Unknowned filter 
type " + t.getOperatorType().identifier());
++        accumulator.add(target);
++    }
++
++    /**
++     * Copies all expressions that are in the given list.
++     * The returned list has the same length than the given list.
++     * If all copied expressions are equal to the original expressions and 
{@link #forceNew} is {@code false},
++     * then this method returns {@code null} for telling that filter or 
expression does not need to be created.
++     *
++     * <h4>Note on parameterized types</h4>
++     * This method cannot guarantee that the elements in the returned list 
have really the parameterized types
++     * declared in this method signature. They <em>should be</em> if the 
{@link FilterFactory}, {@link Filter}
++     * and {@link Expression} methods invoked by the caller fulfill the API 
contract. For example the operands
++     * of an arithmetic function should have {@link Number} value. But we 
have no way to verify that.
++     *
++     * @param  <V>  expected type of values returned by the expressions. This 
type <em>is not verified</em>,
++     *         so this method is not really type-safe. This parameterized 
type is nevertheless used for more
++     *         convenient casts by the callers, but should not be used 
outside private methods.
++     * @param  source  the list of expressions to copy.
++     * @return the copies expressions, or {@code null} if no copy is needed.
++     */
++    @SuppressWarnings({"unchecked","rawtypes"})
++    private <V> List<Expression<TR,V>> copyExpressions(final 
List<Expression<? super SR, ?>> source) {
++        final List<Object> results = new ArrayList<>(source.size());
++        for (final Expression<? super SR, ?> e : source) {
++            visit((Expression<SR,?>) e, results);
+         }
 -        u.set(result);
 -    }
 -
 -    private void copyLogical(Filter<R> t, AtomicReference u) {
 -        final LogicalOperator co = (LogicalOperator) t;
 -        final List<Filter<T>> ops = copyOperands(co.getOperands());
 -        final Filter<T> result;
 -        switch (t.getOperatorType().identifier()) {
 -            case "And" : result = targetFactory.and(ops); break;
 -            case "Or" :  result = targetFactory.or(ops); break;
 -            case "Not" : result = targetFactory.not(ops.get(0)); break;
 -            default : throw new IllegalArgumentException("Unknowned filter 
type " + t.getOperatorType().identifier());
++        // Cast to <TR> is safe because of factory method signatures.
++        return !forceNew && results.equals(source) ? null : (List) results;
++    }
++
++    /**
++     * Copies all filters that are in the given list.
++     * The returned list has the same length than the given list.
++     * If all copied filters are equal to the original filters and {@link 
#forceNew} is {@code false},
++     * then this method returns {@code null} for telling that filter does not 
need to be created.
++     *
++     * @param  source  the list of filters to copy.
++     * @return the copies filters, or {@code null} if no copy is needed.
++     */
++    @SuppressWarnings({"unchecked","rawtypes"})
++    private List<Filter<TR>> copyFilters(final List<Filter<? super SR>> 
source) {
++        final var results = new ArrayList<Object>(source.size());
++        for (final Filter<? super SR> e : source) {
++            visit((Filter<SR>) e, results);
+         }
 -        u.set(result);
 -    }
 -
 -    private void copySpatial(Filter<R> t, AtomicReference u) {
 -        final List<Expression<T, Object>> exps = 
copyExpressions(t.getExpressions());
 -        final Filter<T> result;
 -        switch (t.getOperatorType().identifier()) {
 -            case "BBOX" :       result = targetFactory.bbox(exps.get(0), 
(Envelope) exps.get(1).apply(null)); break;
 -            case "Equals" :     result = targetFactory.equals(exps.get(0), 
exps.get(1)); break;
 -            case "Disjoint" :   result = targetFactory.disjoint(exps.get(0), 
exps.get(1)); break;
 -            case "Intersects" : result = 
targetFactory.intersects(exps.get(0), exps.get(1)); break;
 -            case "Touches" :    result = targetFactory.touches(exps.get(0), 
exps.get(1)); break;
 -            case "Crosses" :    result = targetFactory.crosses(exps.get(0), 
exps.get(1)); break;
 -            case "Within" :     result = targetFactory.within(exps.get(0), 
exps.get(1)); break;
 -            case "Contains" :   result = targetFactory.contains(exps.get(0), 
exps.get(1)); break;
 -            case "Overlaps" :   result = targetFactory.overlaps(exps.get(0), 
exps.get(1)); break;
 -            case "DWithin" :    result = targetFactory.within(exps.get(0), 
exps.get(1), ((DistanceOperator) t).getDistance()); break;
 -            case "Beyond" :     result = targetFactory.beyond(exps.get(0), 
exps.get(1), ((DistanceOperator) t).getDistance()); break;
 -            default : throw new IllegalArgumentException("Unknowned filter 
type " + t.getOperatorType().identifier());
++        // Cast to <TR> is safe because of factory method signatures.
++        return !forceNew && results.equals(source) ? null : (List) results;
++    }
++
++    /**
++     * Copies the given filter using the factory specified at construction 
time.
++     *
++     * @param  source  the filter to copy.
++     * @return the copied filter.
++     * @throws IllegalArgumentException if the filter cannot be copied.
++     */
++    @SuppressWarnings("unchecked")
++    public Filter<TR> copy(final Filter<SR> source) {
++        final var accumulator = new ArrayList<Object>(1);
++        visit(source, accumulator);
++        switch (accumulator.size()) {
++            case 0:  return null;
++            case 1:  return (Filter<TR>) accumulator.get(0);
++            default: throw new AssertionError(accumulator);     // Should 
never happen.
+         }
 -        u.set(result);
 -    }
 -
 -    private void copyMath(Expression<R,?> t, AtomicReference u) {
 -        final List<Expression> exps = (List) 
copyExpressions(t.getParameters());
 -        final Expression<T,?> result;
 -        switch (t.getFunctionName().toString()) {
 -            case FunctionNames.Add : result = targetFactory.add(exps.get(0), 
exps.get(1)); break;
 -            case FunctionNames.Subtract : result = 
targetFactory.subtract(exps.get(0), exps.get(1)); break;
 -            case FunctionNames.Multiply : result = 
targetFactory.multiply(exps.get(0), exps.get(1)); break;
 -            case FunctionNames.Divide : result = 
targetFactory.divide(exps.get(0), exps.get(1)); break;
 -            default : throw new IllegalArgumentException("Unknowned 
expression type " + t.getFunctionName().toString());
++    }
++
++    /**
++     * Copies the given expression using the factory specified at 
construction time.
++     *
++     * @param  <V>     the type of values computed by the expression.
++     * @param  source  the expression to copy.
++     * @return the copied expression.
++     * @throws IllegalArgumentException if the expression cannot be copied.
++     */
++    @SuppressWarnings("unchecked")
++    public <V> Expression<TR,V> copy(final Expression<SR,V> source) {
++        final var accumulator = new ArrayList<Object>(1);
++        visit(source, accumulator);
++        switch (accumulator.size()) {
++            case 0:  return null;
++            case 1:  return (Expression<TR,V>) accumulator.get(0);
++            default: throw new AssertionError(accumulator);     // Should 
never happen.
+         }
 -        u.set(result);
+     }
+ 
++    /**
++     * Invoked when no copy operation is registered for the given filter.
++     * The default implementation throws an {@link IllegalArgumentException}.
++     *
++     * @param  type         the filter type which has not been found.
++     * @param  filter       the filter to copy.
++     * @param  accumulator  where to add filters.
++     * @throws IllegalArgumentException if a copy of the given filter was 
required by cannot be performed.
++     */
+     @Override
 -    protected void typeNotFound(CodeList<?> type, Filter<R> filter, 
AtomicReference u) {
 -        if (filter instanceof BetweenComparisonOperator) {
 -            final BetweenComparisonOperator op = (BetweenComparisonOperator) 
filter;
 -            u.set(targetFactory.between(copy(op.getExpression()), 
copy(op.getLowerBoundary()), copy(op.getUpperBoundary())));
 -        } else if (filter instanceof LikeOperator) {
 -            final LikeOperator<R> op = (LikeOperator) filter;
 -            u.set(targetFactory.like(
 -                    copy(op.getExpressions().get(0)),
 -                    (String) copy(op.getExpressions().get(1)).apply(null),
 -                    op.getWildCard(),
 -                    op.getSingleChar(),
 -                    op.getEscapeChar(),
 -                    op.isMatchingCase()));
 -        } else if (filter instanceof NilOperator) {
 -            final NilOperator<R> op = (NilOperator) filter;
 -            u.set(targetFactory.isNil(
 -                    copy(op.getExpressions().get(0)),
 -                    op.getNilReason().orElse(null)));
 -        } else if (filter instanceof NullOperator) {
 -            final NullOperator<R> op = (NullOperator) filter;
 -            u.set(targetFactory.isNull(copy(op.getExpressions().get(0))));
 -        } else {
 -            super.typeNotFound(type, filter, u);
++    protected void typeNotFound(final CodeList<?> type, final Filter<SR> 
filter, final List<Object> accumulator) {
++        if (forceNew) {
++            throw new 
IllegalArgumentException(Resources.format(Resources.Keys.CanNotVisit_2, 0, 
type));
+         }
++        accumulator.add(filter);
+     }
+ 
++    /**
++     * Invoked when no copy operation is registered for the given expression.
++     * The default implementation creates a new function of the same name 
using the generic API.
++     *
++     * @param  name         the expression type which has not been found.
++     * @param  expression   the expression.
++     * @param  accumulator  where to add expressions.
++     * @throws IllegalArgumentException if an expression cannot be copied.
++     */
+     @Override
 -    protected void typeNotFound(String type, Expression<R, ?> expression, 
AtomicReference u) {
 -        if (expression instanceof Literal) {
 -            final Literal exp = (Literal) expression;
 -            u.set(targetFactory.literal(exp.getValue()));
 -        } else if (expression instanceof ValueReference) {
 -            final ValueReference exp = (ValueReference) expression;
 -            u.set(targetFactory.property(exp.getXPath()));
 -        } else {
 -            u.set(targetFactory.function(type, 
copyExpressions(expression.getParameters()).toArray(new Expression[0])));
++    @SuppressWarnings("unchecked")
++    protected void typeNotFound(final String name, Expression<SR, ?> 
expression, final List<Object> accumulator) {
++        var exps = copyExpressions(expression.getParameters());
++        if (exps != null) {
++            expression = factory.function(name, 
exps.toArray(Expression[]::new));
+         }
++        accumulator.add(expression);
+     }
 -
+ }
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/internal/filter/EditVisitor.java
index 0000000000,0000000000..8250d3db0a
new file mode 100644
--- /dev/null
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/EditVisitor.java
@@@ -1,0 -1,0 +1,56 @@@
++/*
++ * 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.internal.filter;
++
++import org.opengis.filter.FilterFactory;
++
++/**
++ * Visitor used to copy expressions and filters with same parameterized types.
++ * This class can be used when some filters need to be recreated with the 
same types
++ * but potentially different values. For example a change of {@link Literal} 
value
++ * requires to recreate all parents in the filter graph.
++ *
++ * <h2>Partially implemented factory</h2>
++ * {@code EditVisitor} relaxes the usual factory API contract by allowing 
unsupported factory
++ * methods to return {@code null} instead of throwing an {@link 
UnsupportedOperationException}.
++ * A null value is interpreted as an instruction to continue to use the old 
filter or expression,
++ * without replacing it by a new instance created by the {@linkplain 
#factory}.
++ * By contrast, an {@link UnsupportedOperationException} causes the copy 
operation to fail.
++ *
++ * @author  Johann Sorel (Geomatys)
++ * @author  Martin Desruisseaux (Geomatys)
++ * @version 1.4
++ *
++ * @param  <R>  the type of resources expected by the filters and expressions.
++ * @param  <G>  base class of geometry objects.
++ * @param  <T>  base class of temporal objects.
++ *
++ * @since 1.4
++ */
++public class EditVisitor<R,G,T> extends CopyVisitor<R,R,G,T> {
++    /**
++     * Creates a new edit visitor with given factory.
++     * If the {@code force} argument is {@code false}, then the factory is 
used for
++     * creating new filters and expressions only when at least one operand 
changed.
++     *
++     * @param  factory  the factory to use for creating the new filters and 
expressions.
++     * @param  force    whether to force new filters or expressions even when 
existing instances could be reused.
++     */
++    public EditVisitor(final FilterFactory<R,G,T> factory, final boolean 
force) {
++        super(factory, force, force);
++    }
++}
diff --cc 
core/sis-feature/src/main/java/org/apache/sis/internal/filter/package-info.java
index 67864b3b8d,915385a223..bdc668828a
--- 
a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/package-info.java
+++ 
b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/package-info.java
@@@ -20,7 -20,8 +20,7 @@@
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.3
 - * @version 1.1
++ * @version 1.4
   * @since   1.1
 - * @module
   */
  package org.apache.sis.internal.filter;
diff --cc 
core/sis-feature/src/test/java/org/apache/sis/internal/filter/CopyVisitorTest.java
index 0000000000,5fc1ee494b..9495b3c70f
mode 000000,100644..100644
--- 
a/core/sis-feature/src/test/java/org/apache/sis/internal/filter/CopyVisitorTest.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/internal/filter/CopyVisitorTest.java
@@@ -1,0 -1,403 +1,77 @@@
+ /*
+  * 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.internal.filter;
+ 
 -import java.time.Instant;
 -import java.util.Arrays;
 -import java.util.Collection;
 -import java.util.List;
+ import java.util.Map;
 -import javax.measure.Quantity;
 -import javax.measure.quantity.Length;
 -import org.apache.sis.filter.DefaultFilterFactory;
++import org.junit.Test;
+ import org.apache.sis.test.TestCase;
 -import org.apache.sis.util.iso.Names;
++import org.apache.sis.filter.DefaultFilterFactory;
++
+ import static org.junit.Assert.*;
 -import org.junit.Test;
++
++// Branch-dependent imports
+ import org.opengis.feature.Feature;
 -import org.opengis.filter.BetweenComparisonOperator;
 -import org.opengis.filter.BinaryComparisonOperator;
 -import org.opengis.filter.BinarySpatialOperator;
 -import org.opengis.filter.DistanceOperator;
+ import org.opengis.filter.Expression;
 -import org.opengis.filter.Filter;
+ import org.opengis.filter.FilterFactory;
 -import org.opengis.filter.InvalidFilterValueException;
 -import org.opengis.filter.LikeOperator;
 -import org.opengis.filter.Literal;
 -import org.opengis.filter.LogicalOperator;
 -import org.opengis.filter.MatchAction;
 -import org.opengis.filter.NilOperator;
 -import org.opengis.filter.NullOperator;
 -import org.opengis.filter.ResourceId;
 -import org.opengis.filter.SortOrder;
 -import org.opengis.filter.SortProperty;
 -import org.opengis.filter.TemporalOperator;
 -import org.opengis.filter.ValueReference;
 -import org.opengis.filter.Version;
 -import org.opengis.filter.capability.FilterCapabilities;
 -import org.opengis.geometry.Envelope;
 -import org.opengis.metadata.citation.Citation;
 -import org.opengis.util.ScopedName;
++
+ 
+ /**
 - * Verifies copy from {@link CopyVisitor}.
++ * Tests copies using {@link CopyVisitor}.
+  *
 - * @author Johann Sorel (Geomatys)
++ * @author  Johann Sorel (Geomatys)
++ * @version 1.4
++ * @since   1.4
+  */
 -public class CopyVisitorTest extends TestCase {
 -
++public final class CopyVisitorTest extends TestCase {
+     /**
 -     * Test copy a value reference.
++     * Tests copy a value reference.
+      */
+     @Test
+     public void copyValueReference() {
 -        final FilterFactory<Feature,Object,?> source = 
DefaultFilterFactory.forFeatures();
 -        final FilterFactory<Map,Object,Object> target = new MockFactory();
++        final FilterFactory<Feature, ?, ?>                source  = 
DefaultFilterFactory.forFeatures();
++        final FilterFactory<Map<String,?>, ?, ?>          target  = new 
FilterFactoryMock();
++        final CopyVisitor  <Feature, Map<String,?>, ?, ?> visitor = new 
CopyVisitor<>(target, true);
+ 
 -        final Expression<Feature,?> exp = source.property("name");
 -        final Expression<Map, Object> result = new 
CopyVisitor<>(target).copy(exp);
++        final String xpath = "someProperty";
++        final Expression<Feature, ?> exp = source.property(xpath);
++        final Expression<Map<String,?>, ?> result = visitor.copy(exp);
+ 
 -        assertTrue(result instanceof MockValueReference);
++        assertTrue(result instanceof ValueReferenceMock);
++        assertEquals(xpath, ((ValueReferenceMock) result).getXPath());
+     }
+ 
+     /**
 -     * Test copy a function.
++     * Tests copy of a function.
+      */
+     @Test
+     public void copyFunction() {
 -        final FilterFactory<Feature,Object,?> source = 
DefaultFilterFactory.forFeatures();
 -        final FilterFactory<Map,Object,Object> target = new MockFactory();
 -
 -        final Expression<Feature,?> exp1 = source.property("name");
 -        final Expression<Feature,?> exp2 = source.property("crs");
 -        final Expression<Feature,?> fct = source.function("ST_GeomFromText", 
exp1, exp2);
 -        final Expression<Map, Object> result = new 
CopyVisitor<>(target).copy(fct);
 -
 -        assertTrue(result instanceof MockFunction);
 -        final MockFunction resultfct = (MockFunction) result;
 -        assertEquals(2, resultfct.getParameters().size());
 -        assertTrue(resultfct.getParameters().get(0) instanceof 
MockValueReference);
 -        assertTrue(resultfct.getParameters().get(1) instanceof 
MockValueReference);
 -    }
 -}
 -
 -final class MockFactory implements FilterFactory<Map, Object, Object>{
 -
 -    @Override
 -    public FilterCapabilities getCapabilities() {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public ResourceId<Map> resourceId(String rid, Version version, Instant 
startTime, Instant endTime) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public <V> ValueReference property(String xpath, Class<V> type) {
 -        return new MockValueReference(xpath);
 -    }
 -
 -    @Override
 -    public <V> Literal<Map, V> literal(V value) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinaryComparisonOperator<Map> equal(Expression<? super Map, ?> 
expression1, Expression<? super Map, ?> expression2, boolean isMatchingCase, 
MatchAction matchAction) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinaryComparisonOperator<Map> notEqual(Expression<? super Map, ?> 
expression1, Expression<? super Map, ?> expression2, boolean isMatchingCase, 
MatchAction matchAction) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinaryComparisonOperator<Map> less(Expression<? super Map, ?> 
expression1, Expression<? super Map, ?> expression2, boolean isMatchingCase, 
MatchAction matchAction) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinaryComparisonOperator<Map> greater(Expression<? super Map, ?> 
expression1, Expression<? super Map, ?> expression2, boolean isMatchingCase, 
MatchAction matchAction) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinaryComparisonOperator<Map> lessOrEqual(Expression<? super Map, 
?> expression1, Expression<? super Map, ?> expression2, boolean isMatchingCase, 
MatchAction matchAction) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinaryComparisonOperator<Map> greaterOrEqual(Expression<? super 
Map, ?> expression1, Expression<? super Map, ?> expression2, boolean 
isMatchingCase, MatchAction matchAction) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BetweenComparisonOperator<Map> between(Expression<? super Map, ?> 
expression, Expression<? super Map, ?> lowerBoundary, Expression<? super Map, 
?> upperBoundary) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public LikeOperator<Map> like(Expression<? super Map, ?> expression, 
String pattern, char wildcard, char singleChar, char escape, boolean 
isMatchingCase) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public NullOperator<Map> isNull(Expression<? super Map, ?> expression) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public NilOperator<Map> isNil(Expression<? super Map, ?> expression, 
String nilReason) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public LogicalOperator<Map> and(Collection<? extends Filter<? super Map>> 
operands) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public LogicalOperator<Map> or(Collection<? extends Filter<? super Map>> 
operands) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public LogicalOperator<Map> not(Filter<? super Map> operand) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinarySpatialOperator<Map> bbox(Expression<? super Map, ? extends 
Object> geometry, Envelope bounds) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinarySpatialOperator<Map> equals(Expression<? super Map, ? 
extends Object> geometry1, Expression<? super Map, ? extends Object> geometry2) 
{
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
++        final FilterFactory<Feature, ?, ?>                source  = 
DefaultFilterFactory.forFeatures();
++        final FilterFactory<Map<String,?>, ?, ?>          target  = new 
FilterFactoryMock();
++        final CopyVisitor  <Feature, Map<String,?>, ?, ?> visitor = new 
CopyVisitor<>(target, true);
+ 
 -    @Override
 -    public BinarySpatialOperator<Map> disjoint(Expression<? super Map, ? 
extends Object> geometry1, Expression<? super Map, ? extends Object> geometry2) 
{
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinarySpatialOperator<Map> intersects(Expression<? super Map, ? 
extends Object> geometry1, Expression<? super Map, ? extends Object> geometry2) 
{
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinarySpatialOperator<Map> touches(Expression<? super Map, ? 
extends Object> geometry1, Expression<? super Map, ? extends Object> geometry2) 
{
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinarySpatialOperator<Map> crosses(Expression<? super Map, ? 
extends Object> geometry1, Expression<? super Map, ? extends Object> geometry2) 
{
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinarySpatialOperator<Map> within(Expression<? super Map, ? 
extends Object> geometry1, Expression<? super Map, ? extends Object> geometry2) 
{
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinarySpatialOperator<Map> contains(Expression<? super Map, ? 
extends Object> geometry1, Expression<? super Map, ? extends Object> geometry2) 
{
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public BinarySpatialOperator<Map> overlaps(Expression<? super Map, ? 
extends Object> geometry1, Expression<? super Map, ? extends Object> geometry2) 
{
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public DistanceOperator<Map> beyond(Expression<? super Map, ? extends 
Object> geometry1, Expression<? super Map, ? extends Object> geometry2, 
Quantity<Length> distance) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public DistanceOperator<Map> within(Expression<? super Map, ? extends 
Object> geometry1, Expression<? super Map, ? extends Object> geometry2, 
Quantity<Length> distance) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
++        final Expression<Feature, ?> exp1 = source.property("someProperty");
++        final Expression<Feature, ?> exp2 = source.property("crs");
++        final Expression<Feature, ?> fct  = 
source.function("ST_GeomFromText", exp1, exp2);
++        final Expression<Map<String,?>, ?> result = visitor.copy(fct);
+ 
 -    @Override
 -    public TemporalOperator<Map> after(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
++        assertTrue(result instanceof FunctionMock);
++        var resultfct = ((FunctionMock) result).getParameters();
++        assertEquals(2, resultfct.size());
++        assertTrue(resultfct.get(0) instanceof ValueReferenceMock);
++        assertTrue(resultfct.get(1) instanceof ValueReferenceMock);
+     }
 -
 -    @Override
 -    public TemporalOperator<Map> before(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> begins(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> begunBy(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> tcontains(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> during(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> tequals(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> toverlaps(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> meets(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> ends(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> overlappedBy(Expression<? super Map, ? 
extends Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> metBy(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> endedBy(Expression<? super Map, ? extends 
Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public TemporalOperator<Map> anyInteracts(Expression<? super Map, ? 
extends Object> time1, Expression<? super Map, ? extends Object> time2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public Expression<Map, Number> add(Expression<? super Map, ? extends 
Number> operand1, Expression<? super Map, ? extends Number> operand2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public Expression<Map, Number> subtract(Expression<? super Map, ? extends 
Number> operand1, Expression<? super Map, ? extends Number> operand2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public Expression<Map, Number> multiply(Expression<? super Map, ? extends 
Number> operand1, Expression<? super Map, ? extends Number> operand2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public Expression<Map, Number> divide(Expression<? super Map, ? extends 
Number> operand1, Expression<? super Map, ? extends Number> operand2) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public Expression<Map, ?> function(String name, Expression<? super Map, 
?>[] parameters) {
 -        return new MockFunction(name, Arrays.asList(parameters));
 -    }
 -
 -    @Override
 -    public SortProperty<Map> sort(ValueReference<? super Map, ?> property, 
SortOrder order) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public Citation getVendor() {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -}
 -
 -final class MockValueReference implements ValueReference<Map, Object> {
 -
 -    private final String xpath;
 -
 -    public MockValueReference(String xpath) {
 -        this.xpath = xpath;
 -    }
 -
 -    @Override
 -    public String getXPath() {
 -        return xpath;
 -    }
 -
 -    @Override
 -    public Object apply(Map input) throws InvalidFilterValueException {
 -        return input.get(xpath);
 -    }
 -
 -    @Override
 -    public <N> Expression<Map, N> toValueType(Class<N> target) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -}
 -
 -final class MockFunction implements Expression<Map,Object> {
 -
 -    private final String name;
 -    private final List<Expression<? super Map, ?>> parameters;
 -
 -    public MockFunction(String name, List<Expression<? super Map, ?>> 
parameters) {
 -        this.name = name;
 -        this.parameters = parameters;
 -    }
 -
 -    @Override
 -    public ScopedName getFunctionName() {
 -        return Names.createScopedName(null, null, name);
 -    }
 -
 -    @Override
 -    public List<Expression<? super Map, ?>> getParameters() {
 -        return parameters;
 -    }
 -
 -    @Override
 -    public Object apply(Map input) throws InvalidFilterValueException {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
 -    @Override
 -    public <N> Expression<Map, N> toValueType(Class<N> target) {
 -        throw new UnsupportedOperationException("Not supported.");
 -    }
 -
+ }
diff --cc 
core/sis-feature/src/test/java/org/apache/sis/internal/filter/FilterFactoryMock.java
index 0000000000,0000000000..195672c98c
new file mode 100644
--- /dev/null
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/internal/filter/FilterFactoryMock.java
@@@ -1,0 -1,0 +1,562 @@@
++/*
++ * 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.internal.filter;
++
++import java.time.Instant;
++import java.util.Map;
++import java.util.Arrays;
++import java.util.Collection;
++import javax.measure.Quantity;
++import javax.measure.quantity.Length;
++import org.opengis.geometry.Envelope;
++import org.opengis.metadata.citation.Citation;
++import org.apache.sis.internal.simple.SimpleCitation;
++
++// Branch-dependent imports
++import org.opengis.filter.*;
++import org.opengis.filter.capability.FilterCapabilities;
++
++
++/**
++ * Dummy implementation of filter factory.
++ * The features handled by this implementation are property-value maps.
++ *
++ * @author  Johann Sorel (Geomatys)
++ * @version 1.4
++ * @since   1.4
++ */
++final class FilterFactoryMock implements FilterFactory<Map<String,?>, Object, 
Object> {
++    /**
++     * Creates a new dummy factory.
++     */
++    FilterFactoryMock() {
++    }
++
++    /**
++     * Returns the "vendor" responsible for creating this factory 
implementation.
++     */
++    @Override
++    public Citation getVendor() {
++        return new SimpleCitation("SIS-tests");
++    }
++
++    /**
++     * Creates an expression retrieving the value as an instance of the 
specified class.
++     */
++    @Override
++    public <V> ValueReference<Map<String,?>, V> property(String xpath, 
Class<V> type) {
++        return new ValueReferenceMock<>(xpath, type);
++    }
++
++    /**
++     * Creates a dummy function with an arbitrary number of parameters.
++     */
++    @Override
++    public Expression<Map<String,?>, ?> function(String name, Expression<? 
super Map<String,?>, ?>[] parameters) {
++        return new FunctionMock(name, Arrays.asList(parameters));
++    }
++
++    // ======== All operations below this point are unsupported 
===================================================
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public FilterCapabilities getCapabilities() {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public ResourceId<Map<String,?>> resourceId(String rid, Version version, 
Instant startTime, Instant endTime) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public <V> Literal<Map<String,?>, V> literal(V value) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinaryComparisonOperator<Map<String,?>> equal(
++            Expression<? super Map<String,?>, ?> expression1,
++            Expression<? super Map<String,?>, ?> expression2,
++            boolean isMatchingCase, MatchAction matchAction)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinaryComparisonOperator<Map<String,?>> notEqual(
++            Expression<? super Map<String,?>, ?> expression1,
++            Expression<? super Map<String,?>, ?> expression2,
++            boolean isMatchingCase, MatchAction matchAction)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinaryComparisonOperator<Map<String,?>> less(
++            Expression<? super Map<String,?>, ?> expression1,
++            Expression<? super Map<String,?>, ?> expression2,
++            boolean isMatchingCase, MatchAction matchAction)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinaryComparisonOperator<Map<String,?>> greater(
++            Expression<? super Map<String,?>, ?> expression1,
++            Expression<? super Map<String,?>, ?> expression2,
++            boolean isMatchingCase, MatchAction matchAction)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinaryComparisonOperator<Map<String,?>> lessOrEqual(
++            Expression<? super Map<String,?>, ?> expression1,
++            Expression<? super Map<String,?>, ?> expression2,
++            boolean isMatchingCase, MatchAction matchAction)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinaryComparisonOperator<Map<String,?>> greaterOrEqual(
++            Expression<? super Map<String,?>, ?> expression1,
++            Expression<? super Map<String,?>, ?> expression2,
++            boolean isMatchingCase, MatchAction matchAction)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BetweenComparisonOperator<Map<String,?>> between(
++            Expression<? super Map<String,?>, ?> expression,
++            Expression<? super Map<String,?>, ?> lowerBoundary,
++            Expression<? super Map<String,?>, ?> upperBoundary)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public LikeOperator<Map<String,?>> like(
++            Expression<? super Map<String,?>, ?> expression,
++            String pattern, char wildcard, char singleChar, char escape, 
boolean isMatchingCase)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public NullOperator<Map<String,?>> isNull(Expression<? super 
Map<String,?>, ?> expression) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public NilOperator<Map<String,?>> isNil(Expression<? super Map<String,?>, 
?> expression, String nilReason) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public LogicalOperator<Map<String,?>> and(Collection<? extends Filter<? 
super Map<String,?>>> operands) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public LogicalOperator<Map<String,?>> or(Collection<? extends Filter<? 
super Map<String,?>>> operands) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public LogicalOperator<Map<String,?>> not(Filter<? super Map<String,?>> 
operand) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinarySpatialOperator<Map<String,?>> bbox(
++            Expression<? super Map<String,?>, ? extends Object> geometry,
++            Envelope bounds)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinarySpatialOperator<Map<String,?>> equals(
++            Expression<? super Map<String,?>, ? extends Object> geometry1,
++            Expression<? super Map<String,?>, ? extends Object> geometry2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinarySpatialOperator<Map<String,?>> disjoint(
++            Expression<? super Map<String,?>, ? extends Object> geometry1,
++            Expression<? super Map<String,?>, ? extends Object> geometry2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinarySpatialOperator<Map<String,?>> intersects(
++            Expression<? super Map<String,?>, ? extends Object> geometry1,
++            Expression<? super Map<String,?>, ? extends Object> geometry2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinarySpatialOperator<Map<String,?>> touches(
++            Expression<? super Map<String,?>, ? extends Object> geometry1,
++            Expression<? super Map<String,?>, ? extends Object> geometry2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinarySpatialOperator<Map<String,?>> crosses(
++            Expression<? super Map<String,?>, ? extends Object> geometry1,
++            Expression<? super Map<String,?>, ? extends Object> geometry2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinarySpatialOperator<Map<String,?>> within(
++            Expression<? super Map<String,?>, ? extends Object> geometry1,
++            Expression<? super Map<String,?>, ? extends Object> geometry2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinarySpatialOperator<Map<String,?>> contains(
++            Expression<? super Map<String,?>, ? extends Object> geometry1,
++            Expression<? super Map<String,?>, ? extends Object> geometry2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public BinarySpatialOperator<Map<String,?>> overlaps(
++            Expression<? super Map<String,?>, ? extends Object> geometry1,
++            Expression<? super Map<String,?>, ? extends Object> geometry2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public DistanceOperator<Map<String,?>> beyond(
++            Expression<? super Map<String,?>, ? extends Object> geometry1,
++            Expression<? super Map<String,?>, ? extends Object> geometry2,
++            Quantity<Length> distance)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public DistanceOperator<Map<String,?>> within(
++            Expression<? super Map<String,?>, ? extends Object> geometry1,
++            Expression<? super Map<String,?>, ? extends Object> geometry2,
++            Quantity<Length> distance)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> after(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> before(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> begins(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> begunBy(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> tcontains(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> during(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> tequals(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> toverlaps(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> meets(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> ends(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> overlappedBy(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> metBy(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> endedBy(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public TemporalOperator<Map<String,?>> anyInteracts(
++            Expression<? super Map<String,?>, ? extends Object> time1,
++            Expression<? super Map<String,?>, ? extends Object> time2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public Expression<Map<String,?>, Number> add(
++            Expression<? super Map<String,?>, ? extends Number> operand1,
++            Expression<? super Map<String,?>, ? extends Number> operand2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public Expression<Map<String,?>, Number> subtract(
++            Expression<? super Map<String,?>, ? extends Number> operand1,
++            Expression<? super Map<String,?>, ? extends Number> operand2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public Expression<Map<String,?>, Number> multiply(
++            Expression<? super Map<String,?>, ? extends Number> operand1,
++            Expression<? super Map<String,?>, ? extends Number> operand2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public Expression<Map<String,?>, Number> divide(
++            Expression<? super Map<String,?>, ? extends Number> operand1,
++            Expression<? super Map<String,?>, ? extends Number> operand2)
++    {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public SortProperty<Map<String,?>> sort(ValueReference<? super 
Map<String,?>, ?> property, SortOrder order) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++}
diff --cc 
core/sis-feature/src/test/java/org/apache/sis/internal/filter/FunctionMock.java
index 0000000000,0000000000..cae63d9653
new file mode 100644
--- /dev/null
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/internal/filter/FunctionMock.java
@@@ -1,0 -1,0 +1,91 @@@
++/*
++ * 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.internal.filter;
++
++import java.util.Map;
++import java.util.List;
++import org.opengis.util.ScopedName;
++import org.apache.sis.util.iso.Names;
++
++// Branch-dependent imports
++import org.opengis.filter.Expression;
++
++
++/**
++ * Dummy implementation of filter function.
++ * The features handled by this implementation are property-value maps.
++ * This class stores a function name and parameters but cannot do any 
operation.
++ *
++ * @author  Johann Sorel (Geomatys)
++ * @version 1.4
++ * @since   1.4
++ */
++final class FunctionMock implements Expression<Map<String,?>, Object> {
++    /**
++     * The local part of the function name.
++     */
++    private final String name;
++
++    /**
++     * The function parameters.
++     */
++    private final List<Expression<? super Map<String,?>, ?>> parameters;
++
++    /**
++     * Creates a new dummy function.
++     *
++     * @param  name        the local part of the function name.
++     * @param  parameters  the function parameters.
++     */
++    FunctionMock(final String name, final List<Expression<? super 
Map<String,?>, ?>> parameters) {
++        this.name = name;
++        this.parameters = parameters;
++    }
++
++    /**
++     * Returns the function name.
++     */
++    @Override
++    public ScopedName getFunctionName() {
++        return Names.createScopedName(null, null, name);
++    }
++
++    /**
++     * Returns the function parameters.
++     */
++    @Override
++    @SuppressWarnings("ReturnOfCollectionOrArrayField")
++    public List<Expression<? super Map<String,?>, ?>> getParameters() {
++        return parameters;
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public Object apply(Map<String,?> feature) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++
++    /**
++     * Unsupported operation.
++     */
++    @Override
++    public <N> Expression<Map<String,?>, N> toValueType(Class<N> target) {
++        throw new UnsupportedOperationException("Not supported.");
++    }
++}
diff --cc 
core/sis-feature/src/test/java/org/apache/sis/internal/filter/ValueReferenceMock.java
index 0000000000,0000000000..90eb9a1f75
new file mode 100644
--- /dev/null
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/internal/filter/ValueReferenceMock.java
@@@ -1,0 -1,0 +1,82 @@@
++/*
++ * 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.internal.filter;
++
++import java.util.Map;
++
++// Branch-dependent imports
++import org.opengis.filter.Expression;
++import org.opengis.filter.ValueReference;
++
++
++/**
++ * Dummy implementation of property value reference.
++ * The features handled by this implementation are property-value maps.
++ *
++ * @author  Johann Sorel (Geomatys)
++ * @version 1.4
++ *
++ * @param  <V>  type of values returned by this expression.
++ *
++ * @since 1.4
++ */
++final class ValueReferenceMock<V> implements ValueReference<Map<String,?>, V> 
{
++    /**
++     * Name of the property for which to get values.
++     */
++    private final String xpath;
++
++    /**
++     * Expected type of values.
++     */
++    private final Class<V> type;
++
++    /**
++     * Creates a new dummy reference.
++     *
++     * @param xpath  name of the property for which to get values.
++     * @param type   type of values returned by this expression.
++     */
++    ValueReferenceMock(final String xpath, final Class<V> type) {
++        this.xpath = xpath;
++        this.type  = type;
++    }
++
++    /**
++     * Returns the name of the property for which to get values.
++     */
++    @Override
++    public String getXPath() {
++        return xpath;
++    }
++
++    /**
++     * Returns the value of the referenced property in the given feature.
++     */
++    @Override
++    public V apply(final Map<String,?> feature) {
++        return type.cast(feature.get(xpath));
++    }
++
++    /**
++     * Returns a reference to the same value but casted to a different type.
++     */
++    @Override
++    public <N> Expression<Map<String,?>, N> toValueType(final Class<N> 
target) {
++        return new ValueReferenceMock<>(xpath, target);
++    }
++}
diff --cc 
core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index 2c588d0a13,16a86d5a21..aa285fcdcf
--- 
a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ 
b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@@ -26,8 -26,9 +26,8 @@@ import org.junit.runners.Suite
   *
   * @author  Martin Desruisseaux (Geomatys)
   * @author  Johann Sorel (Geomatys)
-  * @version 1.3
 - * @version 1.2
++ * @version 1.4
   * @since   0.5
 - * @module
   */
  @Suite.SuiteClasses({
      org.apache.sis.feature.DefaultAttributeTypeTest.class,


Reply via email to