sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Refator the filter implementation in order to share more common implementations. Also try to perform a more elaborated analysis of types during arithmetic operations. This work is not final; we still need to do a deeper review of filter interfaces: https://github.com/opengeospatial/geoapi/issues/32
Date Sat, 08 Jun 2019 12:02:25 GMT
This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 76d0e34  Refator the filter implementation in order to share more common implementations. Also try to perform a more elaborated analysis of types during arithmetic operations. This work is not final; we still need to do a deeper review of filter interfaces: https://github.com/opengeospatial/geoapi/issues/32
76d0e34 is described below

commit 76d0e3404cb51c1c42b4a125e3051a8996ff809f
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat Jun 8 13:59:59 2019 +0200

    Refator the filter implementation in order to share more common implementations.
    Also try to perform a more elaborated analysis of types during arithmetic operations.
    This work is not final; we still need to do a deeper review of filter interfaces:
    https://github.com/opengeospatial/geoapi/issues/32
---
 .../filter/AbstractBinaryComparisonOperator.java   | 219 ---------
 .../sis/filter/AbstractBinaryExpression.java       | 116 -----
 .../apache/sis/filter/AbstractBinaryOperator.java  | 114 -----
 .../sis/filter/AbstractComparisonOperator.java     | 114 -----
 .../org/apache/sis/filter/AbstractExpression.java  | 102 -----
 .../apache/sis/filter/AbstractUnaryOperator.java   |  94 ----
 .../org/apache/sis/filter/ArithmeticFunction.java  | 252 ++++++++++
 .../java/org/apache/sis/filter/BinaryFunction.java | 186 ++++++++
 .../org/apache/sis/filter/ComparisonFunction.java  | 508 +++++++++++++++++++++
 .../java/org/apache/sis/filter/DefaultAdd.java     |  80 ----
 .../java/org/apache/sis/filter/DefaultAnd.java     | 101 ----
 .../java/org/apache/sis/filter/DefaultDivide.java  |  80 ----
 .../org/apache/sis/filter/DefaultFeatureId.java    |  92 ----
 .../apache/sis/filter/DefaultFilterFactory.java    | 194 ++++----
 .../main/java/org/apache/sis/filter/DefaultId.java | 131 ------
 .../java/org/apache/sis/filter/DefaultLiteral.java | 148 ------
 .../org/apache/sis/filter/DefaultMultiply.java     |  80 ----
 .../java/org/apache/sis/filter/DefaultNot.java     |  91 ----
 .../org/apache/sis/filter/DefaultObjectId.java     | 111 +++++
 .../main/java/org/apache/sis/filter/DefaultOr.java | 101 ----
 .../sis/filter/DefaultPropertyIsEqualTo.java       |  96 ----
 .../sis/filter/DefaultPropertyIsGreaterThan.java   |  64 ---
 .../DefaultPropertyIsGreaterThanOrEqualTo.java     |  64 ---
 .../sis/filter/DefaultPropertyIsLessThan.java      |  64 ---
 .../filter/DefaultPropertyIsLessThanOrEqualTo.java |  64 ---
 .../apache/sis/filter/DefaultPropertyIsLike.java   |  93 ----
 .../apache/sis/filter/DefaultPropertyIsNull.java   |  73 ---
 .../org/apache/sis/filter/DefaultPropertyName.java | 156 -------
 .../org/apache/sis/filter/DefaultSubtract.java     |  81 ----
 .../org/apache/sis/filter/FilterByIdentifier.java  | 136 ++++++
 .../java/org/apache/sis/filter/LeafExpression.java | 254 +++++++++++
 .../org/apache/sis/filter/LogicalFunction.java     | 158 +++++++
 .../src/main/java/org/apache/sis/filter/Node.java  | 171 +++++++
 .../java/org/apache/sis/filter/UnaryFunction.java  | 170 +++++++
 .../sis/internal/feature/FeatureExpression.java    |   2 +-
 .../apache/sis/filter/ArithmeticFunctionTest.java  |  80 ++++
 .../java/org/apache/sis/filter/DefaultAddTest.java |  63 ---
 .../java/org/apache/sis/filter/DefaultAndTest.java | 104 -----
 .../org/apache/sis/filter/DefaultDivideTest.java   |  63 ---
 .../org/apache/sis/filter/DefaultLiteralTest.java  |  74 ---
 .../org/apache/sis/filter/DefaultMultiplyTest.java |  63 ---
 .../java/org/apache/sis/filter/DefaultNotTest.java |  79 ----
 ...FeatureIdTest.java => DefaultObjectIdTest.java} |  66 +--
 .../java/org/apache/sis/filter/DefaultOrTest.java  | 104 -----
 .../apache/sis/filter/DefaultPropertyNameTest.java |  80 ----
 .../org/apache/sis/filter/DefaultSubtractTest.java |  63 ---
 ...aultIdTest.java => FilterByIdentifierTest.java} |  33 +-
 .../org/apache/sis/filter/LeafExpressionTest.java  | 123 +++++
 .../org/apache/sis/filter/LogicalFunctionTest.java | 146 ++++++
 .../org/apache/sis/filter/UnaryFunctionTest.java   |  76 +++
 .../apache/sis/test/suite/FeatureTestSuite.java    |  17 +-
 .../sis/internal/converter/DateConverter.java      |  15 +-
 .../org/apache/sis/internal/system/Loggers.java    |   7 +-
 53 files changed, 2542 insertions(+), 3274 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryComparisonOperator.java b/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryComparisonOperator.java
deleted file mode 100644
index fd1e664..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryComparisonOperator.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Date;
-import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.ObjectConverters;
-import org.opengis.filter.BinaryComparisonOperator;
-import org.opengis.filter.MatchAction;
-import org.opengis.filter.expression.Expression;
-
-/**
- * Immutable abstract "binary comparison operator".
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-abstract class AbstractBinaryComparisonOperator extends AbstractBinaryOperator implements BinaryComparisonOperator,Serializable{
-
-    protected final boolean match;
-    protected final MatchAction matchAction;
-
-    public AbstractBinaryComparisonOperator(final Expression left, final Expression right, final boolean match, final MatchAction matchAction) {
-        super(left, right);
-        this.match = match;
-        this.matchAction = matchAction;
-        ArgumentChecks.ensureNonNull("matchAction", matchAction);
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public boolean isMatchingCase() {
-        return match;
-    }
-
-    @Override
-    public MatchAction getMatchAction() {
-        return matchAction;
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public final boolean evaluate(final Object candidate) {
-        final Object objleft = expression1.evaluate(candidate);
-        final Object objright = expression2.evaluate(candidate);
-
-        if (objleft instanceof Collection && objright instanceof Collection) {
-            return evaluateOne(objleft, objright);
-        } else if (objleft instanceof Collection) {
-            final Collection col = (Collection) objleft;
-            if (col.isEmpty()) return false;
-            switch (matchAction) {
-                case ALL:
-                    for (Object o : col) {
-                        if (!evaluateOne(o, objright)) return false;
-                    }
-                    return true;
-                case ANY:
-                    for (Object o : col) {
-                        if (evaluateOne(o, objright)) return true;
-                    }
-                    return false;
-                case ONE:
-                    boolean found = false;
-                    for (Object o : col) {
-                        if (evaluateOne(o, objright)) {
-                            if (found) return false;
-                            found = true;
-                        }
-                    }
-                    return found;
-                default: return false;
-            }
-        } else if (objright instanceof Collection) {
-            final Collection col = (Collection) objright;
-            if (col.isEmpty()) return false;
-            switch (matchAction) {
-                case ALL:
-                    for (Object o : col) {
-                        if (!evaluateOne(objleft,o)) return false;
-                    }
-                    return true;
-                case ANY:
-                    for (Object o : col) {
-                        if (evaluateOne(objleft,o)) return true;
-                    }
-                    return false;
-                case ONE:
-                    boolean found = false;
-                    for (Object o : col) {
-                        if (evaluateOne(objleft,o)) {
-                            if (found) return false;
-                            found = true;
-                        }
-                    }
-                    return found;
-                default: return false;
-            }
-        } else {
-            return evaluateOne(objleft, objright);
-        }
-    }
-
-    protected abstract boolean evaluateOne(Object objleft,Object objright);
-
-    protected Integer compare(Object objleft, Object objright){
-
-        if (!(objleft instanceof Comparable)) {
-            return null;
-        }
-
-        //see if the right type might be more appropriate for test
-        if ( !(objleft instanceof Date) ){
-
-            if (objright instanceof Date) {
-                //object right class is more appropriate
-
-                Object cdLeft = ObjectConverters.convert(objleft, Date.class);
-                if (cdLeft != null) {
-                    return ((Comparable) cdLeft).compareTo(objright);
-                }
-
-            }
-
-        }
-
-        objright = ObjectConverters.convert(objright, objleft.getClass());
-
-        if (objleft instanceof java.sql.Date && objright instanceof java.sql.Date) {
-            final Calendar cal1 = Calendar.getInstance();
-            cal1.setTime((java.sql.Date) objleft);
-            cal1.set(Calendar.HOUR_OF_DAY, 0);
-            cal1.set(Calendar.MINUTE, 0);
-            cal1.set(Calendar.SECOND, 0);
-            cal1.set(Calendar.MILLISECOND, 0);
-
-            final Calendar cal2 = Calendar.getInstance();
-            cal2.setTime((java.sql.Date) objright);
-            cal2.set(Calendar.HOUR_OF_DAY, 0);
-            cal2.set(Calendar.MINUTE, 0);
-            cal2.set(Calendar.SECOND, 0);
-            cal2.set(Calendar.MILLISECOND, 0);
-
-            return cal1.compareTo(cal2);
-        }
-
-        if (objright == null) {
-            return null;
-        }
-        return ((Comparable) objleft).compareTo(objright);
-    }
-
-
-    @Override
-    public int hashCode() {
-        int hash = 7;
-        hash = 61 * hash + (this.expression1 != null ? this.expression1.hashCode() : 0);
-        hash = 61 * hash + (this.expression2 != null ? this.expression2.hashCode() : 0);
-        hash = 61 * hash + (this.match ? 1 : 0);
-        return hash;
-    }
-
-    @Override
-    public boolean equals(final Object obj) {
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final AbstractBinaryComparisonOperator other = (AbstractBinaryComparisonOperator) obj;
-        if (this.expression1 != other.expression1 && (this.expression1 == null || !this.expression1.equals(other.expression1))) {
-            return false;
-        }
-        if (this.expression2 != other.expression2 && (this.expression2 == null || !this.expression2.equals(other.expression2))) {
-            return false;
-        }
-        if (this.match != other.match) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public String toString() {
-        final StringBuilder sb = new StringBuilder(getClass().getSimpleName() + " (matchcase=");
-        sb.append(match).append(")\n");
-        sb.append(AbstractExpression.toStringTree("",Arrays.asList(expression1,expression2)));
-        return sb.toString();
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryExpression.java b/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryExpression.java
deleted file mode 100644
index 9cce5d6..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryExpression.java
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import org.opengis.filter.expression.BinaryExpression;
-import org.opengis.filter.expression.Expression;
-
-
-/**
- * Base class for expressions performing operations on two values.
- * The nature of the operation is dependent on the subclass.
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-abstract class AbstractBinaryExpression extends AbstractExpression implements BinaryExpression, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -4430693449544064634L;
-
-    /**
-     * The first of the two expressions to be used by this operator.
-     *
-     * @see #getExpression1()
-     */
-    protected final Expression expression1;
-
-    /**
-     * The second of the two expressions to be used by this operator.
-     *
-     * @see #getExpression2()
-     */
-    protected final Expression expression2;
-
-    /**
-     * Creates a new binary operator.
-     * It is caller responsibility to ensure that no argument is null.
-     */
-    AbstractBinaryExpression(final Expression expression1, final Expression expression2) {
-        this.expression1 = expression1;
-        this.expression2 = expression2;
-    }
-
-    /**
-     * Returns the mathematical symbol for this binary operator.
-     * For comparison operators, the symbol should be one of the following:
-     * {@literal < > ≤ ≥ = ≠}.
-     */
-    protected abstract char symbol();
-
-    /**
-     * Returns the first of the two expressions to be used by this operator.
-     */
-    @Override
-    public final Expression getExpression1() {
-        return expression1;
-    }
-
-    /**
-     * Returns the second of the two expressions to be used by this operator.
-     */
-    @Override
-    public final Expression getExpression2() {
-        return expression2;
-    }
-
-    /**
-     * Returns a hash code value for this operator.
-     */
-    @Override
-    public int hashCode() {
-        // We use the symbol as a way to differentiate the subclasses.
-        return (31 * expression1.hashCode() + expression2.hashCode()) ^ symbol();
-    }
-
-    /**
-     * Compares this operator with the given object for equality.
-     */
-    @Override
-    public boolean equals(final Object obj) {
-        if (obj != null && obj.getClass() == getClass()) {
-            final AbstractBinaryExpression other = (AbstractBinaryExpression) obj;
-            return expression1.equals(other.expression1) &&
-                   expression2.equals(other.expression2);
-        }
-        return false;
-    }
-
-    /**
-     * Returns a string representation of this operator.
-     */
-    @Override
-    public String toString() {
-        return new StringBuilder(30).append(expression1).append(' ').append(symbol()).append(' ')
-                                    .append(expression2).toString();
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryOperator.java b/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryOperator.java
deleted file mode 100644
index f7a2dab..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryOperator.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import org.opengis.filter.Filter;
-import org.opengis.filter.expression.Expression;
-
-
-/**
- * Base class for filters performing operations on two values.
- * The nature of the operation is dependent on the subclass.
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-abstract class AbstractBinaryOperator implements Filter, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -5079157703540568112L;
-
-    /**
-     * The first of the two expressions to be used by this operator.
-     *
-     * @see #getExpression1()
-     */
-    protected final Expression expression1;
-
-    /**
-     * The second of the two expressions to be used by this operator.
-     *
-     * @see #getExpression2()
-     */
-    protected final Expression expression2;
-
-    /**
-     * Creates a new binary operator.
-     * It is caller responsibility to ensure that no argument is null.
-     */
-    AbstractBinaryOperator(final Expression expression1, final Expression expression2) {
-        this.expression1 = expression1;
-        this.expression2 = expression2;
-    }
-
-    /**
-     * Returns the mathematical symbol for this binary operator.
-     * For comparison operators, the symbol should be one of the following:
-     * {@literal < > ≤ ≥ = ≠}.
-     */
-    protected abstract char symbol();
-
-    /**
-     * Returns the first of the two expressions to be used by this operator.
-     */
-    public final Expression getExpression1() {
-        return expression1;
-    }
-
-    /**
-     * Returns the second of the two expressions to be used by this operator.
-     */
-    public final Expression getExpression2() {
-        return expression2;
-    }
-
-    /**
-     * Returns a hash code value for this operator.
-     */
-    @Override
-    public int hashCode() {
-        // We use the symbol as a way to differentiate the subclasses.
-        return (31 * expression1.hashCode() + expression2.hashCode()) ^ symbol();
-    }
-
-    /**
-     * Compares this operator with the given object for equality.
-     */
-    @Override
-    public boolean equals(final Object obj) {
-        if (obj != null && obj.getClass() == getClass()) {
-            final AbstractBinaryOperator other = (AbstractBinaryOperator) obj;
-            return expression1.equals(other.expression1) &&
-                   expression2.equals(other.expression2);
-        }
-        return false;
-    }
-
-    /**
-     * Returns a string representation of this operator.
-     */
-    @Override
-    public String toString() {
-        return new StringBuilder(30).append(expression1).append(' ').append(symbol()).append(' ')
-                                    .append(expression2).toString();
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java b/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java
deleted file mode 100644
index cd99903..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import org.opengis.filter.BinaryComparisonOperator;
-import org.opengis.filter.MatchAction;
-import org.opengis.filter.expression.Expression;
-
-
-/**
- * Base class for filters that compare exactly two values against each other.
- * The nature of the comparison is dependent on the subclass.
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-abstract class AbstractComparisonOperator extends AbstractBinaryOperator implements BinaryComparisonOperator, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -4709016194087609721L;
-
-    /**
-     * Whether comparisons are case sensitive.
-     *
-     * @see #isMatchingCase()
-     */
-    protected final boolean matchCase;
-
-    /**
-     * Specifies how the comparison predicate shall be evaluated for a collection of values.
-     *
-     * @see #getMatchAction()
-     */
-    protected final MatchAction matchAction;
-
-    /**
-     * Creates a new binary comparison operator.
-     * It is caller responsibility to ensure that no argument is null.
-     */
-    AbstractComparisonOperator(final Expression expression1, final Expression expression2,
-                               final boolean matchCase, final MatchAction matchAction)
-    {
-        super(expression1, expression2);
-        this.matchCase   = matchCase;
-        this.matchAction = matchAction;
-    }
-
-    /**
-     * Specifies whether comparisons are case sensitive.
-     *
-     * @return {@code true} if the comparisons are case sensitive, otherwise {@code false}.
-     */
-    @Override
-    public final boolean isMatchingCase() {
-        return matchCase;
-    }
-
-    /**
-     * Specifies how the comparison predicate shall be evaluated for a collection of values.
-     * Values can be {@link MatchAction#ALL ALL} if all values in the collection shall satisfy the predicate,
-     * {@link MatchAction#ANY ANY} if any of the value in the collection can satisfy the predicate, or
-     * {@link MatchAction#ONE ONE} if only one of the values in the collection shall satisfy the predicate.
-     *
-     * @return how the comparison predicate shall be evaluated for a collection of values.
-     */
-    @Override
-    public final MatchAction getMatchAction() {
-        return matchAction;
-    }
-
-    /**
-     * Returns a hash code value for this comparison operator.
-     */
-    @Override
-    public final int hashCode() {
-        int hash = super.hashCode() * 37 + matchAction.hashCode();
-        if (matchCase) hash = ~hash;
-        return hash;
-    }
-
-    /**
-     * Compares this operator with the given object for equality.
-     */
-    @Override
-    public final boolean equals(final Object obj) {
-        if (obj == this) {
-            return true;
-        }
-        if (super.equals(obj)) {
-            final AbstractComparisonOperator other = (AbstractComparisonOperator) obj;
-            return matchCase == other.matchCase && matchAction.equals(other.matchAction);
-        }
-        return false;
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractExpression.java b/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractExpression.java
deleted file mode 100644
index 9201b7a..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractExpression.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Iterator;
-import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.ObjectConverters;
-import org.apache.sis.util.UnconvertibleObjectException;
-import org.apache.sis.internal.feature.FeatureExpression;
-
-// Branch-dependent imports
-import org.opengis.feature.FeatureType;
-import org.opengis.filter.expression.Expression;
-
-
-/**
- * Base class of Apache SIS implementation of OGC expressions operating on feature instances.
- * This base class adds an additional method, {@link #expectedType(FeatureType)}, for fetching
- * in advance the expected type of expression results.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-abstract class AbstractExpression implements Expression, FeatureExpression {
-    /**
-     * Creates a new expression.
-     */
-    protected AbstractExpression() {
-    }
-
-    /**
-     * Evaluates the expression for producing a result of the given type.
-     * The default implementation evaluate the expression in the default
-     * way and attempt to convert the result.
-     *
-     * @param  feature  to feature to evaluate with this expression.
-     * @param  target   the desired type for the expression result.
-     */
-    @Override
-    public <T> T evaluate(final Object feature, final Class<T> target) {
-        ArgumentChecks.ensureNonNull("target", target);
-        final Object value = evaluate(feature);
-        try {
-            return ObjectConverters.convert(value, target);
-        } catch (UnconvertibleObjectException ex) {
-            // TODO: should report the exception somewhere.
-            return null;
-        }
-    }
-
-
-    /**
-     * Returns a graphical representation of the specified objects. This representation can be
-     * printed to the {@linkplain System#out standard output stream} (for example) if it uses
-     * a monospaced font and supports unicode.
-     *
-     * @param  root  The root name of the tree to format.
-     * @param  objects The objects to format as root children.
-     * @return A string representation of the tree.
-     */
-    static String toStringTree(String root, final Iterable<?> objects) {
-        final StringBuilder sb = new StringBuilder();
-        if (root != null) {
-            sb.append(root);
-        }
-        if (objects != null) {
-            final Iterator<?> ite = objects.iterator();
-            while (ite.hasNext()) {
-                sb.append('\n');
-                final Object next = ite.next();
-                final boolean last = !ite.hasNext();
-                sb.append(last ? "└─ " : "├─ ");
-
-                final String[] parts = String.valueOf(next).split("\n");
-                sb.append(parts[0]);
-                for (int k=1;k<parts.length;k++) {
-                    sb.append('\n');
-                    sb.append(last ? ' ' : '│');
-                    sb.append("  ");
-                    sb.append(parts[k]);
-                }
-            }
-        }
-        return sb.toString();
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractUnaryOperator.java b/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractUnaryOperator.java
deleted file mode 100644
index ea0bddc..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractUnaryOperator.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import org.opengis.filter.Filter;
-import org.opengis.filter.expression.Expression;
-
-
-/**
- * Base class for filters performing operations on one value.
- * The nature of the operation is dependent on the subclass.
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-abstract class AbstractUnaryOperator implements Filter, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -8183962550739028650L;
-
-    /**
-     * The expression to be used by this operator.
-     *
-     * @see #getExpression()
-     */
-    protected final Expression expression;
-
-    /**
-     * Creates a new unary operator.
-     * It is caller responsibility to ensure that no argument is null.
-     */
-    AbstractUnaryOperator(final Expression expression) {
-        this.expression = expression;
-    }
-
-    /**
-     * Returns the mathematical symbol for this operator.
-     */
-    protected abstract char symbol();
-
-    /**
-     * Returns the expressions to be used by this operator.
-     */
-    public final Expression getExpression() {
-        return expression;
-    }
-
-    /**
-     * Returns a hash code value for this operator.
-     */
-    @Override
-    public int hashCode() {
-        // We use the symbol as a way to differentiate the subclasses.
-        return expression.hashCode() ^ symbol();
-    }
-
-    /**
-     * Compares this operator with the given object for equality.
-     */
-    @Override
-    public boolean equals(final Object obj) {
-        if (obj != null && obj.getClass() == getClass()) {
-            return expression.equals(((AbstractUnaryOperator) obj).expression);
-        }
-        return false;
-    }
-
-    /**
-     * Returns a string representation of this operator.
-     */
-    @Override
-    public String toString() {
-        return new StringBuilder(30).append(expression).append(':').append(symbol()).toString();
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/ArithmeticFunction.java b/core/sis-feature/src/main/java/org/apache/sis/filter/ArithmeticFunction.java
new file mode 100644
index 0000000..1188df8
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ArithmeticFunction.java
@@ -0,0 +1,252 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import org.apache.sis.util.Numbers;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.internal.feature.FeatureExpression;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+import org.opengis.filter.expression.BinaryExpression;
+import org.opengis.filter.expression.Expression;
+import org.opengis.filter.expression.ExpressionVisitor;
+
+
+/**
+ * Arithmetic operations between two numerical values.
+ * The nature of the operation depends on the subclass.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+abstract class ArithmeticFunction extends BinaryFunction implements BinaryExpression, FeatureExpression {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 2818625862630588268L;
+
+    /**
+     * Creates a new arithmetic function.
+     */
+    ArithmeticFunction(final Expression expression1, final Expression expression2) {
+        super(expression1, expression2);
+    }
+
+    /**
+     * Creates an attribute type for numeric values of the given name.
+     * The attribute is mandatory, unbounded and has no default value.
+     *
+     * @param  name  name of the attribute to create.
+     * @return an attribute of the given name for numbers.
+     */
+    static AttributeType<Number> createNumericType(final String name) {
+        return createType(Number.class, name);
+    }
+
+    /**
+     * Evaluates this expression based on the content of the given object. This method delegates to
+     * {@link #applyAsDouble(double, double)}, {@link #applyAsLong(long, long)} or similar methods
+     * depending on the value types.
+     *
+     * @throws ArithmeticException if the operation overflows the capacity of the type used.
+     */
+    @Override
+    public final Object evaluate(final Object feature) {
+        return evaluate(feature, Number.class);
+    }
+
+    /**
+     * Evaluates the expression for producing a result of the given type. This method delegates to
+     * {@link #applyAsDouble(double, double)}, {@link #applyAsLong(long, long)} or similar methods
+     * depending on the value types. If this method can not produce a value of the given type,
+     * then it returns {@code null}.
+     *
+     * @param  feature  to feature to evaluate with this expression.
+     * @param  target   the desired type for the expression result.
+     * @return the result, or {@code null} if it can not be of the specified type.
+     * @throws ClassCastException if an expression returned the value in an expected type.
+     * @throws ArithmeticException if the operation overflows the capacity of the type used.
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public final <T> T evaluate(final Object feature, final Class<T> target) {
+        ArgumentChecks.ensureNonNull("target", target);
+        if (Number.class.isAssignableFrom(target)) {
+            final Number left = (Number) expression1.evaluate(feature, target);
+            if (left != null) {
+                final Number right = (Number) expression2.evaluate(feature, target);
+                if (right != null) {
+                    return (T) Numbers.cast(apply(left, right), (Class<? extends Number>) target);
+                }
+            }
+        }
+        return null;
+    }
+
+
+    /**
+     * The "Add" (+) expression.
+     */
+    static final class Add extends ArithmeticFunction implements org.opengis.filter.expression.Add {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 5445433312445869201L;
+
+        /** Description of results of the {@value #NAME} expression. */
+        private static final AttributeType<Number> TYPE = createNumericType(NAME);
+        @Override public PropertyType expectedType(FeatureType type) {return TYPE;}
+
+        /** Creates a new expression for the {@value #NAME} operation. */
+        Add(Expression expression1, Expression expression2) {
+            super(expression1, expression2);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '+';}
+
+        /** Applies this expression to the given operands. */
+        @Override protected Number applyAsDouble (double     left, double     right) {return left + right;}
+        @Override protected Number applyAsDecimal(BigDecimal left, BigDecimal right) {return left.add(right);}
+        @Override protected Number applyAsInteger(BigInteger left, BigInteger right) {return left.add(right);}
+        @Override protected Number applyAsLong   (long       left, long       right) {return Math.addExact(left, right);}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(ExpressionVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+
+
+    /**
+     * The "Sub" (−) expression.
+     */
+    static final class Subtract extends ArithmeticFunction implements org.opengis.filter.expression.Subtract {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 3048878022726271508L;
+
+        /** Description of results of the {@value #NAME} expression. */
+        private static final AttributeType<Number> TYPE = createNumericType(NAME);
+        @Override public PropertyType expectedType(FeatureType type) {return TYPE;}
+
+        /** Creates a new expression for the {@value #NAME} operation. */
+        Subtract(Expression expression1, Expression expression2) {
+            super(expression1, expression2);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '−';}
+
+        /** Applies this expression to the given operands. */
+        @Override protected Number applyAsDouble (double     left, double     right) {return left - right;}
+        @Override protected Number applyAsDecimal(BigDecimal left, BigDecimal right) {return left.subtract(right);}
+        @Override protected Number applyAsInteger(BigInteger left, BigInteger right) {return left.subtract(right);}
+        @Override protected Number applyAsLong   (long       left, long       right) {return Math.subtractExact(left, right);}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(ExpressionVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+
+
+    /**
+     * The "Mul" (×) expression.
+     */
+    static final class Multiply extends ArithmeticFunction implements org.opengis.filter.expression.Multiply {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = -1300022614832645625L;
+
+        /** Description of results of the {@value #NAME} expression. */
+        private static final AttributeType<Number> TYPE = createNumericType(NAME);
+        @Override public PropertyType expectedType(FeatureType type) {return TYPE;}
+
+        /** Creates a new expression for the {@value #NAME} operation. */
+        Multiply(Expression expression1, Expression expression2) {
+            super(expression1, expression2);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '×';}
+
+        /** Applies this expression to the given operands. */
+        @Override protected Number applyAsDouble (double     left, double     right) {return left * right;}
+        @Override protected Number applyAsDecimal(BigDecimal left, BigDecimal right) {return left.multiply(right);}
+        @Override protected Number applyAsInteger(BigInteger left, BigInteger right) {return left.multiply(right);}
+        @Override protected Number applyAsLong   (long       left, long       right) {return Math.multiplyExact(left, right);}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(ExpressionVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+
+
+    /**
+     * The "Div" (÷) expression.
+     */
+    static final class Divide extends ArithmeticFunction implements org.opengis.filter.expression.Divide {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = -7709291845568648891L;
+
+        /** Description of results of the {@value #NAME} expression. */
+        private static final AttributeType<Number> TYPE = createNumericType(NAME);
+        @Override public PropertyType expectedType(FeatureType type) {return TYPE;}
+
+        /** Creates a new expression for the {@value #NAME} operation. */
+        Divide(Expression expression1, Expression expression2) {
+            super(expression1, expression2);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '÷';}
+
+        /** Applies this expression to the given operands. */
+        @Override protected Number applyAsDouble (double     left, double     right) {return left / right;}
+        @Override protected Number applyAsDecimal(BigDecimal left, BigDecimal right) {return left.divide(right);}
+        @Override protected Number applyAsInteger(BigInteger left, BigInteger right) {
+            BigInteger[] r = left.divideAndRemainder(right);
+            if (BigInteger.ZERO.equals(r[1])) {
+                return r[0];
+            } else {
+                return left.doubleValue() / right.doubleValue();
+            }
+        }
+
+        /** Divides the given integers, changing the type to a floating point type if the result is not an integer. */
+        @Override protected Number applyAsLong(final long left, final long right) {
+            if (left % right == 0) {
+                return left / right;
+            } else {
+                return left / (double) right;
+            }
+        }
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(ExpressionVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryFunction.java b/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryFunction.java
new file mode 100644
index 0000000..e119afe
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryFunction.java
@@ -0,0 +1,186 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.math.BigInteger;
+import java.math.BigDecimal;
+import org.apache.sis.util.Numbers;
+import org.apache.sis.math.DecimalFunctions;
+import org.apache.sis.util.ArgumentChecks;
+
+// Branch-dependent imports
+import org.opengis.filter.expression.Expression;
+
+
+/**
+ * Base class for expressions, comparators or filters performing operations on two expressions.
+ * The nature of the operation depends on the subclass. If operands are numerical values, they
+ * may be converted to a common type before the operation is performed. That operation is not
+ * necessarily an arithmetic operation; it may be a comparison for example.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+abstract class BinaryFunction extends Node {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -8632475810190545852L;
+
+    /**
+     * The first of the two expressions to be used by this function.
+     *
+     * @see #getExpression1()
+     */
+    protected final Expression expression1;
+
+    /**
+     * The second of the two expressions to be used by this function.
+     *
+     * @see #getExpression2()
+     */
+    protected final Expression expression2;
+
+    /**
+     * Creates a new binary function.
+     *
+     * @param  expression1  the first of the two expressions to be used by this function.
+     * @param  expression2  the second of the two expressions to be used by this function.
+     */
+    protected BinaryFunction(final Expression expression1, final Expression expression2) {
+        ArgumentChecks.ensureNonNull("expression1", expression1);
+        ArgumentChecks.ensureNonNull("expression2", expression2);
+        this.expression1 = expression1;
+        this.expression2 = expression2;
+    }
+
+    /**
+     * Returns the first of the two expressions to be used by this function.
+     * This is the value specified at construction time.
+     */
+    public final Expression getExpression1() {
+        return expression1;
+    }
+
+    /**
+     * Returns the second of the two expressions to be used by this function.
+     * This is the value specified at construction time.
+     */
+    public final Expression getExpression2() {
+        return expression2;
+    }
+
+    /**
+     * Returns the two expressions in a list of size 2.
+     * This is used for {@link #toString()} implementation.
+     */
+    @Override
+    protected final Collection<?> getChildren() {
+        return Arrays.asList(expression1, expression2);
+    }
+
+    /**
+     * Evaluates the expression for producing a result of numeric type.
+     * This method delegates to one of the {@code applyAs(…)} methods.
+     *
+     * @param  left   the left operand. Can not be null.
+     * @param  right  the right operand. Can not be null.
+     * @return result of this function applied on the two given operands.
+     * @throws ArithmeticException if the operation overflows the capacity of the type used.
+     */
+    protected final Number apply(final Number left, final Number right) {
+        switch (Math.max(Numbers.getEnumConstant(left.getClass()),
+                         Numbers.getEnumConstant(right.getClass())))
+        {
+            case Numbers.BIG_DECIMAL: {
+                return applyAsDecimal(Numbers.cast(left,  BigDecimal.class),
+                                      Numbers.cast(right, BigDecimal.class));
+            }
+            case Numbers.BIG_INTEGER: {
+                return applyAsInteger(Numbers.cast(left,  BigInteger.class),
+                                      Numbers.cast(right, BigInteger.class));
+            }
+            case Numbers.LONG:
+            case Numbers.INTEGER:
+            case Numbers.SHORT:
+            case Numbers.BYTE: {
+                return applyAsLong(left.longValue(), right.longValue());
+            }
+        }
+        return applyAsDouble((left  instanceof Float) ? DecimalFunctions.floatToDouble((Float) left)  : left.doubleValue(),
+                             (right instanceof Float) ? DecimalFunctions.floatToDouble((Float) right) : right.doubleValue());
+    }
+
+    /**
+     * Calculates this function using given operands of {@code long} primitive type. If this function is a
+     * filter, then this method should returns an {@link Integer} value 0 or 1 for false or true respectively.
+     * Otherwise the result is usually a {@link Long}, except for division which may produce a floating point number.
+     *
+     * @throws ArithmeticException if the operation overflows the 64 bits integer capacity.
+     */
+    protected abstract Number applyAsLong(long left, long right);
+
+    /**
+     * Calculates this function using given operands of {@code double} primitive type. If this function is a
+     * filter, then this method should returns an {@link Integer} value 0 or 1 for false or true respectively.
+     * Otherwise the result is usually a {@link Double}.
+     */
+    protected abstract Number applyAsDouble(double left, double right);
+
+    /**
+     * Calculates this function using given operands of {@code BigInteger} type. If this function is a filter,
+     * then this method should returns an {@link Integer} value 0 or 1 for false or true respectively.
+     * Otherwise the result is usually a {@link BigInteger}.
+     */
+    protected abstract Number applyAsInteger(BigInteger left, BigInteger right);
+
+    /**
+     * Calculates this function using given operands of {@code BigDecimal} type. If this function is a filter,
+     * then this method should returns an {@link Integer} value 0 or 1 for false or true respectively.
+     * Otherwise the result is usually a {@link BigDecimal}.
+     */
+    protected abstract Number applyAsDecimal(BigDecimal left, BigDecimal right);
+
+    /**
+     * Returns a hash code value for this function.
+     */
+    @Override
+    public int hashCode() {
+        return (31 * expression1.hashCode() + expression2.hashCode()) ^ getClass().hashCode();
+    }
+
+    /**
+     * Compares this function with the given object for equality.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj != null && obj.getClass() == getClass()) {
+            final BinaryFunction other = (BinaryFunction) obj;
+            return expression1.equals(other.expression1) &&
+                   expression2.equals(other.expression2);
+        }
+        return false;
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/ComparisonFunction.java b/core/sis-feature/src/main/java/org/apache/sis/filter/ComparisonFunction.java
new file mode 100644
index 0000000..0140c0d
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ComparisonFunction.java
@@ -0,0 +1,508 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Date;
+import java.util.Calendar;
+import java.time.Instant;
+import java.time.OffsetDateTime;
+import java.time.chrono.ChronoLocalDate;
+import org.apache.sis.util.ArgumentChecks;
+
+// Branch-dependent imports
+import org.opengis.filter.MatchAction;
+import org.opengis.filter.expression.Expression;
+import org.opengis.filter.BinaryComparisonOperator;
+import org.opengis.filter.FilterVisitor;
+
+
+/**
+ * Comparison operators between two values. Values are converted to the same before comparison, using a widening conversion
+ * (for example from {@link Integer} to {@link Double}). If values can not be compared because they can not be converted to
+ * a common type, or because a value is null or NaN, then the comparison result if {@code false}. A consequence of this rule
+ * is that the two conditions {@literal A < B} and {@literal A ≧ B} may be false in same time.
+ *
+ * <p>If one operand is a collection, all collection elements may be compared to the other value.
+ * Null elements in the collection (not to be confused with null operands) are ignored.
+ * If both operands are collections, current implementation returns {@code false}.</p>
+ *
+ * <p>Comparisons of numerical types shall be done by overriding one of the {@code applyAs…} methods and
+ * returning 0 if {@code false} or 1 if {@code true}. Comparisons of other types is done by overriding
+ * the {@code compare(…)} methods.</p>
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+abstract class ComparisonFunction extends BinaryFunction implements BinaryComparisonOperator {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 1228683039737814926L;
+
+    /**
+     * Specifies whether comparisons are case sensitive.
+     */
+    private final boolean isMatchingCase;
+
+    /**
+     * Specifies how the comparisons shall be evaluated for a collection of values.
+     * Values can be ALL, ANY or ONE.
+     */
+    private final MatchAction matchAction;
+
+    /**
+     * Creates a new comparator.
+     *
+     * @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.
+     */
+    ComparisonFunction(final Expression expression1, final Expression expression2, final boolean isMatchingCase, final MatchAction matchAction) {
+        super(expression1, expression2);
+        this.isMatchingCase = isMatchingCase;
+        this.matchAction = matchAction;
+        ArgumentChecks.ensureNonNull("matchAction", matchAction);
+    }
+
+    /**
+     * Returns whether comparisons are case sensitive.
+     */
+    @Override
+    public final boolean isMatchingCase() {
+        return isMatchingCase;
+    }
+
+    /**
+     * Returns how the comparisons are evaluated for a collection of values.
+     */
+    @Override
+    public final MatchAction getMatchAction() {
+        return matchAction;
+    }
+
+    /**
+     * Takes in account the additional properties in hash code calculation.
+     */
+    @Override
+    public final int hashCode() {
+        return super.hashCode() + Boolean.hashCode(isMatchingCase) + 61 * matchAction.hashCode();
+    }
+
+    /**
+     * Takes in account the additional properties in object comparison.
+     */
+    @Override
+    public final boolean equals(final Object obj) {
+        if (super.equals(obj)) {
+            final ComparisonFunction other = (ComparisonFunction) obj;
+            return other.isMatchingCase == isMatchingCase && matchAction.equals(other.matchAction);
+        }
+        return false;
+    }
+
+    /**
+     * Determines if the test(s) represented by this filter passes with the given operands.
+     * Values of {@link #expression1} and {@link #expression2} can be two single values,
+     * or at most one expression can produce a collection.
+     */
+    @Override
+    public final boolean evaluate(final Object candidate) {
+        final Object left = expression1.evaluate(candidate);
+        if (left != null) {
+            final Object right = expression2.evaluate(candidate);
+            if (right != null) {
+                final Iterable<?> collection;
+                final boolean collectionFirst = (left instanceof Iterable<?>);
+                if (collectionFirst) {
+                    if (right instanceof Iterable<?>) {
+                        // Current implementation does not support collection on both sides. See class javadoc.
+                        return false;
+                    }
+                    collection = (Iterable<?>) left;
+                } else if (right instanceof Iterable<?>) {
+                    collection = (Iterable<?>) right;
+                } else {
+                    return evaluate(left, right);
+                }
+                /*
+                 * At this point, exactly one of the operands is a collection. It may be the left or right one.
+                 * All values in the collection may be compared to the other value until match condition is met.
+                 * Null elements in the collection are ignored.
+                 */
+                boolean found  = false;
+                boolean hasOne = false;
+                for (final Object element : collection) {
+                    if (element != null) {
+                        found = true;
+                        final boolean pass;
+                        if (collectionFirst) {
+                            pass = evaluate(element, right);
+                        } else {
+                            pass = evaluate(left, element);
+                        }
+                        switch (matchAction) {
+                            default:  return false;                            // Unknown enumeration.
+                            case ALL: if (!pass) return false; else break;
+                            case ANY: if ( pass) return true;  else break;
+                            case ONE: {
+                                if (pass) {
+                                    if (hasOne) return false;
+                                    hasOne = true;
+                                }
+                            }
+                        }
+                    }
+                }
+                return found;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Compares the given objects. If both values are numerical, then this method delegates to an {@code applyAs…} method.
+     * For other kind of objects, this method delegates to a {@code compare(…)} method. If the two objects are not of the
+     * same type, then the less accurate one is converted to the most accurate type if possible.
+     *
+     * @param  left   the first object to compare. Must be non-null.
+     * @param  right  the second object to compare. Must be non-null.
+     */
+    @SuppressWarnings("null")
+    private boolean evaluate(final Object left, final Object right) {
+        if (left instanceof Number) {
+            if (right instanceof Number) {
+                final Number r = apply((Number) left, (Number) right);
+                if (r != null) return r.intValue() != 0;
+            }
+            return false;       // Incompatible types.
+        }
+        if (left instanceof CharSequence || right instanceof CharSequence) {            // Really ||, not &&.
+            final String s1 = left.toString();
+            final String s2 = right.toString();
+            final int result;
+            if (isMatchingCase) {
+                result = s1.compareTo(s2);
+            } else {
+                result = s1.compareToIgnoreCase(s2);        // TODO: use Collator for taking locale in account.
+            }
+            return fromCompareTo(result);
+        }
+        /*
+         * Temporal objects. They have somewhat complex conversions.
+         */
+        if (left instanceof Instant) {
+            final Instant t = toInstant(right);
+            if (t != null) return compare((Instant) left, t);
+        } else if (right instanceof Instant) {
+            final Instant t = toInstant(left);
+            if (t != null) return compare(t, (Instant) right);
+        }
+        if (left instanceof OffsetDateTime && right instanceof OffsetDateTime) {
+            return compare((OffsetDateTime) left, (OffsetDateTime) right);
+        }
+        if (left instanceof ChronoLocalDate) {
+            final ChronoLocalDate t = toLocalDate(right);
+            if (t != null) return compare((ChronoLocalDate) left, t);
+        } else if (right instanceof ChronoLocalDate) {
+            final ChronoLocalDate t = toLocalDate(left);
+            return (t != null) && compare(t, (ChronoLocalDate) right);
+        }
+        if (left instanceof Date && right instanceof Date) {
+            return compare((Date) left, (Date) right);
+        }
+        /*
+         * Comparison using `compareTo` method should be last because it does not take in account
+         * the `isMatchingCase` flag and because the semantic is different than < or > comparator
+         * for numbers and dates.
+         */
+        if (left.getClass() == right.getClass() && (left instanceof Comparable<?>)) {
+            @SuppressWarnings("unchecked")
+            final int result = ((Comparable) left).compareTo(right);
+            return fromCompareTo(result);
+        }
+        return false;
+    }
+
+    /**
+     * Converts the given object to an {@link Instant}, or returns {@code null} if unconvertible.
+     * This method handles a few types from the {@link java.time} package and legacy types like
+     * {@link Date} (with a special case for SQL dates) and {@link Calendar}.
+     */
+    private static Instant toInstant(final Object value) {
+        if (value instanceof Instant) {
+            return (Instant) value;
+        } else if (value instanceof OffsetDateTime) {
+            return ((OffsetDateTime) value).toInstant();
+        } else if (value instanceof Date) {
+            if (value instanceof java.sql.Date) {
+                return Instant.ofEpochMilli(((java.sql.Date) value).getTime());
+            } else if (!(value instanceof java.sql.Time)) {
+                return ((Date) value).toInstant();              // Not allowed on java.sql.Date/Time.
+            }
+        } else if (value instanceof Calendar) {
+            return ((Calendar) value).toInstant();
+        }
+        return null;
+    }
+
+    /**
+     * Converts the given object to a {@link ChronoLocalDate}, or returns {@code null} if unconvertible.
+     * This method handles the case of legacy SQL {@link java.sql.Date} objects.
+     */
+    private static ChronoLocalDate toLocalDate(final Object value) {
+        if (value instanceof ChronoLocalDate) {
+            return (ChronoLocalDate) value;
+        } else if (value instanceof java.sql.Date) {
+            return ((java.sql.Date) value).toLocalDate();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Converts the boolean result as an integer for use as a return value of the {@code applyAs…} methods.
+     * This is a helper class for subclasses.
+     */
+    static Number number(final boolean result) {
+        return result ? 1 : 0;
+    }
+
+    /**
+     * Converts the result of {@link Comparable#compareTo(Object)}.
+     */
+    protected abstract boolean fromCompareTo(int result);
+
+    /**
+     * Compares two dates with time-zone information.
+     */
+    protected abstract boolean compare(OffsetDateTime left, OffsetDateTime right);
+
+    /**
+     * Compares two dates without time-of-day and time-zone information.
+     */
+    protected abstract boolean compare(ChronoLocalDate left, ChronoLocalDate right);
+
+    /** Compares two instantaneous points on the time-line. */
+    protected boolean compare(Date    left, Date    right) {return fromCompareTo(left.compareTo(right));}
+    protected boolean compare(Instant left, Instant right) {return fromCompareTo(left.compareTo(right));}
+
+    /** Delegates to {@link BigDecimal#compareTo(BigDecimal)} and interprets the result with {@link #fromCompareTo(int)}. */
+    @Override protected final Number applyAsDecimal(BigDecimal left, BigDecimal right) {return number(fromCompareTo(left.compareTo(right)));}
+    @Override protected final Number applyAsInteger(BigInteger left, BigInteger right) {return number(fromCompareTo(left.compareTo(right)));}
+
+
+    /**
+     * The "LessThan" {@literal (<)} expression.
+     */
+    static final class LessThan extends ComparisonFunction implements org.opengis.filter.PropertyIsLessThan {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 6126039112844823196L;
+
+        /** Creates a new expression for the {@value #NAME} operation. */
+        LessThan(Expression expression1, Expression expression2, boolean isMatchingCase, MatchAction matchAction) {
+            super(expression1, expression2, isMatchingCase, matchAction);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '<';}
+
+        /** Converts {@link Comparable#compareTo(Object)} result to this filter result. */
+        @Override protected boolean fromCompareTo(final int result) {return result < 0;}
+
+        /** Performs the comparison and returns the result as 0 (false) or 1 (true). */
+        @Override protected Number  applyAsDouble(double          left, double          right) {return number(left < right);}
+        @Override protected Number  applyAsLong  (long            left, long            right) {return number(left < right);}
+        @Override protected boolean compare      (Date            left, Date            right) {return left.  before(right);}
+        @Override protected boolean compare      (Instant         left, Instant         right) {return left.isBefore(right);}
+        @Override protected boolean compare      (OffsetDateTime  left, OffsetDateTime  right) {return left.isBefore(right);}
+        @Override protected boolean compare      (ChronoLocalDate left, ChronoLocalDate right) {return left.isBefore(right);}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(FilterVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+
+
+    /**
+     * The "LessThanOrEqualTo" (≤) expression.
+     */
+    static final class LessThanOrEqualTo extends ComparisonFunction implements org.opengis.filter.PropertyIsLessThanOrEqualTo {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 6357459227911760871L;
+
+        /** Creates a new expression for the {@value #NAME} operation. */
+        LessThanOrEqualTo(Expression expression1, Expression expression2, boolean isMatchingCase, MatchAction matchAction) {
+            super(expression1, expression2, isMatchingCase, matchAction);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '≤';}
+
+        /** Converts {@link Comparable#compareTo(Object)} result to this filter result. */
+        @Override protected boolean fromCompareTo(final int result) {return result <= 0;}
+
+        /** Performs the comparison and returns the result as 0 (false) or 1 (true). */
+        @Override protected Number  applyAsDouble(double          left, double          right) {return number(left <= right);}
+        @Override protected Number  applyAsLong  (long            left, long            right) {return number(left <= right);}
+        @Override protected boolean compare      (OffsetDateTime  left, OffsetDateTime  right) {return left.isBefore(right) || left.isEqual(right);}
+        @Override protected boolean compare      (ChronoLocalDate left, ChronoLocalDate right) {return left.isBefore(right) || left.isEqual(right);}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(FilterVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+
+
+    /**
+     * The "GreaterThan" {@literal (>)} expression.
+     */
+    static final class GreaterThan extends ComparisonFunction implements org.opengis.filter.PropertyIsGreaterThan {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 8605517892232632586L;
+
+        /** Creates a new expression for the {@value #NAME} operation. */
+        GreaterThan(Expression expression1, Expression expression2, boolean isMatchingCase, MatchAction matchAction) {
+            super(expression1, expression2, isMatchingCase, matchAction);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '>';}
+
+        /** Converts {@link Comparable#compareTo(Object)} result to this filter result. */
+        @Override protected boolean fromCompareTo(final int result) {return result > 0;}
+
+        /** Performs the comparison and returns the result as 0 (false) or 1 (true). */
+        @Override protected Number  applyAsDouble(double          left, double          right) {return number(left > right);}
+        @Override protected Number  applyAsLong  (long            left, long            right) {return number(left > right);}
+        @Override protected boolean compare      (Date            left, Date            right) {return left.  after(right);}
+        @Override protected boolean compare      (Instant         left, Instant         right) {return left.isAfter(right);}
+        @Override protected boolean compare      (OffsetDateTime  left, OffsetDateTime  right) {return left.isAfter(right);}
+        @Override protected boolean compare      (ChronoLocalDate left, ChronoLocalDate right) {return left.isAfter(right);}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(FilterVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+
+
+    /**
+     * The "GreaterThanOrEqualTo" (≥) expression.
+     */
+    static final class GreaterThanOrEqualTo extends ComparisonFunction implements org.opengis.filter.PropertyIsGreaterThanOrEqualTo {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 1514185657159141882L;
+
+        /** Creates a new expression for the {@value #NAME} operation. */
+        GreaterThanOrEqualTo(Expression expression1, Expression expression2, boolean isMatchingCase, MatchAction matchAction) {
+            super(expression1, expression2, isMatchingCase, matchAction);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '≥';}
+
+        /** Converts {@link Comparable#compareTo(Object)} result to this filter result. */
+        @Override protected boolean fromCompareTo(final int result) {return result >= 0;}
+
+        /** Performs the comparison and returns the result as 0 (false) or 1 (true). */
+        @Override protected Number  applyAsDouble(double          left, double          right) {return number(left >= right);}
+        @Override protected Number  applyAsLong  (long            left, long            right) {return number(left >= right);}
+        @Override protected boolean compare      (OffsetDateTime  left, OffsetDateTime  right) {return left.isAfter(right) || left.isEqual(right);}
+        @Override protected boolean compare      (ChronoLocalDate left, ChronoLocalDate right) {return left.isAfter(right) || left.isEqual(right);}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(FilterVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+
+
+    /**
+     * The "EqualTo" (=) expression.
+     */
+    static final class EqualTo extends ComparisonFunction implements org.opengis.filter.PropertyIsEqualTo {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 8502612221498749667L;
+
+        /** Creates a new expression for the {@value #NAME} operation. */
+        EqualTo(Expression expression1, Expression expression2, boolean isMatchingCase, MatchAction matchAction) {
+            super(expression1, expression2, isMatchingCase, matchAction);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '=';}
+
+        /** Converts {@link Comparable#compareTo(Object)} result to this filter result. */
+        @Override protected boolean fromCompareTo(final int result) {return result == 0;}
+
+        /** Performs the comparison and returns the result as 0 (false) or 1 (true). */
+        @Override protected Number  applyAsDouble(double          left, double          right) {return number(left == right);}
+        @Override protected Number  applyAsLong  (long            left, long            right) {return number(left == right);}
+        @Override protected boolean compare      (OffsetDateTime  left, OffsetDateTime  right) {return left.isEqual(right);}
+        @Override protected boolean compare      (ChronoLocalDate left, ChronoLocalDate right) {return left.isEqual(right);}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(FilterVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+
+
+    /**
+     * The "NotEqualTo" (≠) expression.
+     */
+    static final class NotEqualTo extends ComparisonFunction implements org.opengis.filter.PropertyIsNotEqualTo {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = -3295957142249035362L;
+
+        /** Creates a new expression for the {@value #NAME} operation. */
+        NotEqualTo(Expression expression1, Expression expression2, boolean isMatchingCase, MatchAction matchAction) {
+            super(expression1, expression2, isMatchingCase, matchAction);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '≠';}
+
+        /** Converts {@link Comparable#compareTo(Object)} result to this filter result. */
+        @Override protected boolean fromCompareTo(final int result) {return result != 0;}
+
+        /** Performs the comparison and returns the result as 0 (false) or 1 (true). */
+        @Override protected Number  applyAsDouble(double          left, double          right) {return number(left != right);}
+        @Override protected Number  applyAsLong  (long            left, long            right) {return number(left != right);}
+        @Override protected boolean compare      (OffsetDateTime  left, OffsetDateTime  right) {return !left.isEqual(right);}
+        @Override protected boolean compare      (ChronoLocalDate left, ChronoLocalDate right) {return !left.isEqual(right);}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(FilterVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultAdd.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultAdd.java
deleted file mode 100644
index b5d2c60..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultAdd.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Collections;
-import org.apache.sis.feature.DefaultAttributeType;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyType;
-import org.opengis.filter.expression.Add;
-import org.opengis.filter.expression.Expression;
-import org.opengis.filter.expression.ExpressionVisitor;
-
-/**
- * Addition expression.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class DefaultAdd extends AbstractBinaryExpression implements Add {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = 7251500772368619980L;
-
-    private static final AttributeType<Number> EXPECTED_TYPE = new DefaultAttributeType<>(
-            Collections.singletonMap(DefaultAttributeType.NAME_KEY, NAME),
-                                          Number.class, 1, 1, null, (AttributeType<?>[]) null);
-
-    public DefaultAdd(Expression expressoin1, Expression expression2) {
-        super(expressoin1, expression2);
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(ExpressionVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    protected char symbol() {
-        return '+';
-    }
-
-    @Override
-    public Object evaluate(Object object) {
-        final Double val1 = expression1.evaluate(object, Double.class);
-        final Double val2 = expression2.evaluate(object, Double.class);
-
-        if (val1 == null || val2 == null) {
-            return null;
-        }
-
-        return val1 + val2;
-    }
-
-    @Override
-    public PropertyType expectedType(FeatureType type) {
-        return EXPECTED_TYPE;
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultAnd.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultAnd.java
deleted file mode 100644
index 86c50c1..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultAnd.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import org.opengis.filter.And;
-import org.opengis.filter.Filter;
-import org.opengis.filter.FilterVisitor;
-
-/**
- * Binary logic filter AND.
- * All children filters must be true to pass.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class DefaultAnd implements And, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -8726983485969293693L;
-
-    private final List<Filter> filters;
-
-    public DefaultAnd(List<Filter> filters) {
-        if (filters.size() < 2) throw new IllegalArgumentException("At least two filters are requiered");
-        this.filters = Collections.unmodifiableList(filters);
-    }
-
-    /**
-     * {@inheritDoc}
-     * @return list of at least two {@code Filter}.
-     */
-    @Override
-    public List<Filter> getChildren() {
-        return filters;
-    }
-
-    @Override
-    public boolean evaluate(Object object) {
-        for (Filter filter : filters) {
-            if (!filter.evaluate(object)) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(FilterVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final DefaultAnd other = (DefaultAnd) obj;
-        return Objects.equals(this.filters, other.filters);
-    }
-
-    @Override
-    public int hashCode() {
-        return 43 * filters.hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return AbstractExpression.toStringTree("And", filters);
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultDivide.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultDivide.java
deleted file mode 100644
index b184707..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultDivide.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Collections;
-import org.apache.sis.feature.DefaultAttributeType;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyType;
-import org.opengis.filter.expression.Divide;
-import org.opengis.filter.expression.Expression;
-import org.opengis.filter.expression.ExpressionVisitor;
-
-/**
- * Division expression.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class DefaultDivide extends AbstractBinaryExpression implements Divide {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -3857311165395402436L;
-
-    private static final AttributeType<Number> EXPECTED_TYPE = new DefaultAttributeType<>(
-            Collections.singletonMap(DefaultAttributeType.NAME_KEY, NAME),
-                                          Number.class, 1, 1, null, (AttributeType<?>[]) null);
-
-    public DefaultDivide(Expression expressoin1, Expression expression2) {
-        super(expressoin1, expression2);
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(ExpressionVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    protected char symbol() {
-        return '/';
-    }
-
-    @Override
-    public Object evaluate(Object object) {
-        final Double val1 = expression1.evaluate(object, Double.class);
-        final Double val2 = expression2.evaluate(object, Double.class);
-
-        if (val1 == null || val2 == null) {
-            return null;
-        }
-
-        return val1 / val2;
-    }
-
-    @Override
-    public PropertyType expectedType(FeatureType type) {
-        return EXPECTED_TYPE;
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFeatureId.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFeatureId.java
deleted file mode 100644
index 368fc17..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFeatureId.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import java.util.Objects;
-import org.apache.sis.internal.feature.AttributeConvention;
-import org.apache.sis.util.ArgumentChecks;
-import org.opengis.feature.Feature;
-import org.opengis.feature.PropertyNotFoundException;
-import org.opengis.filter.identity.FeatureId;
-import org.opengis.filter.identity.GmlObjectId;
-
-/**
- * Filter feature id.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class DefaultFeatureId implements FeatureId, GmlObjectId, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -2877500277700165269L;
-
-    private final String id;
-
-    public DefaultFeatureId(String id) {
-        ArgumentChecks.ensureNonNull("id", id);
-        this.id = id;
-    }
-
-    @Override
-    public String getID() {
-        return id;
-    }
-
-    @Override
-    public boolean matches(Object feature) {
-        if (feature instanceof Feature) {
-            final Feature f = (Feature) feature;
-            try {
-                Object id = f.getPropertyValue(AttributeConvention.IDENTIFIER);
-                return this.id.equals(String.valueOf(id));
-            } catch (PropertyNotFoundException ex) {
-                //normal
-            }
-        }
-        return false;
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final DefaultFeatureId other = (DefaultFeatureId) obj;
-        return Objects.equals(this.id, other.id);
-    }
-
-    @Override
-    public int hashCode() {
-        return 31 * id.hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return "Id:" + id;
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
index dc1456b..0f3ec0f 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
@@ -30,7 +30,6 @@ import org.opengis.filter.capability.SpatialOperator;       // Resolve ambiguity
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.Geometry;
 import org.opengis.util.GenericName;
-import org.apache.sis.util.ArgumentChecks;
 
 
 /**
@@ -91,8 +90,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public BBOX bbox(final Expression e, final Envelope bounds)
-    {
+    public BBOX bbox(final Expression e, final Envelope bounds) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -185,7 +183,8 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public DWithin dwithin(final String propertyName, final Geometry geometry,
-            final double distance, final String units) {
+            final double distance, final String units)
+    {
         final PropertyName name = property(propertyName);
         final Literal geom = literal(geometry);
         return dwithin(name, geom, distance, units);
@@ -196,7 +195,8 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public DWithin dwithin(final Expression left, final Expression right,
-            final double distance, final String units) {
+            final double distance, final String units)
+    {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -297,7 +297,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public FeatureId featureId(final String id) {
-        return new DefaultFeatureId(id);
+        return new DefaultObjectId(id);
     }
 
     /**
@@ -305,7 +305,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public GmlObjectId gmlObjectId(final String id) {
-        return new DefaultFeatureId(id);
+        return new DefaultObjectId(id);
     }
 
     // FILTERS /////////////////////////////////////////////////////////////////
@@ -315,8 +315,6 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public And and(final Filter filter1, final Filter filter2) {
-        ArgumentChecks.ensureNonNull("filter1", filter1);
-        ArgumentChecks.ensureNonNull("filter2", filter2);
         return and(Arrays.asList(filter1, filter2));
     }
 
@@ -325,7 +323,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public And and(final List<Filter> filters) {
-        return new DefaultAnd(filters);
+        return new LogicalFunction.And(filters);
     }
 
     /**
@@ -333,8 +331,6 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Or or(final Filter filter1, final Filter filter2) {
-        ArgumentChecks.ensureNonNull("filter1", filter1);
-        ArgumentChecks.ensureNonNull("filter2", filter2);
         return or(Arrays.asList(filter1, filter2));
     }
 
@@ -343,7 +339,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Or or(final List<Filter> filters) {
-        return new DefaultOr(filters);
+        return new LogicalFunction.Or(filters);
     }
 
     /**
@@ -351,7 +347,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Not not(final Filter filter) {
-        return new DefaultNot(filter);
+        return new UnaryFunction.Not(filter);
     }
 
     /**
@@ -359,7 +355,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Id id(final Set<? extends Identifier> ids) {
-        return new DefaultId(ids);
+        return new FilterByIdentifier(ids);
     }
 
     /**
@@ -371,20 +367,20 @@ public class DefaultFilterFactory implements FilterFactory2 {
     }
 
     /**
-     * {@inheritDoc }
+     * Creates a new expression retrieving values from a property of the given name.
+     *
+     * @param  name  name of the property (usually a feature attribute).
      */
     @Override
     public PropertyName property(final String name) {
-        ArgumentChecks.ensureNonNull("name", name);
-        return new DefaultPropertyName(name);
+        return new LeafExpression.Property(name);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsBetween between(final Expression expr,
-            final Expression lower, final Expression upper) {
+    public PropertyIsBetween between(final Expression expression, final Expression lower, final Expression upper) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -392,8 +388,8 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsEqualTo equals(final Expression expr1, final Expression expr2) {
-        return equal(expr1, expr2, true, MatchAction.ANY);
+    public PropertyIsEqualTo equals(final Expression expression1, final Expression expression2) {
+        return equal(expression1, expression2, true, MatchAction.ANY);
     }
 
     /**
@@ -401,127 +397,128 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public PropertyIsEqualTo equal(final Expression expression1, final Expression expression2,
-                                   final boolean matchCase, final MatchAction matchAction)
+                                   final boolean isMatchingCase, final MatchAction matchAction)
     {
-        ArgumentChecks.ensureNonNull("expression1", expression1);
-        ArgumentChecks.ensureNonNull("expression2", expression2);
-        ArgumentChecks.ensureNonNull("matchAction", matchAction);
-        return new DefaultPropertyIsEqualTo(expression1, expression2, matchCase, matchAction);
+        return new ComparisonFunction.EqualTo(expression1, expression2, isMatchingCase, matchAction);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsNotEqualTo notEqual(final Expression expr1, final Expression expr2) {
-        return notEqual(expr1, expr2,false, MatchAction.ANY);
+    public PropertyIsNotEqualTo notEqual(final Expression expression1, final Expression expression2) {
+        return notEqual(expression1, expression2, true, MatchAction.ANY);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsNotEqualTo notEqual(final Expression expr1,
-            final Expression expr2, final boolean matchCase, final MatchAction matchAction) {
-        throw new UnsupportedOperationException("Not supported yet.");
+    public PropertyIsNotEqualTo notEqual(final Expression expression1, final Expression expression2,
+                                         final boolean isMatchingCase, final MatchAction matchAction)
+    {
+        return new ComparisonFunction.NotEqualTo(expression1, expression2, isMatchingCase, matchAction);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsGreaterThan greater(final Expression expr1,
-            final Expression expr2) {
-        return greater(expr1,expr2,false, MatchAction.ANY);
+    public PropertyIsGreaterThan greater(final Expression expression1, final Expression expression2) {
+        return greater(expression1,expression2,false, MatchAction.ANY);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsGreaterThan greater(final Expression expr1,
-            final Expression expr2, final boolean matchCase, final MatchAction matchAction) {
-        return new DefaultPropertyIsGreaterThan(expr1, expr2, matchCase, matchAction);
+    public PropertyIsGreaterThan greater(final Expression expression1, final Expression expression2,
+                                         final boolean isMatchingCase, final MatchAction matchAction)
+    {
+        return new ComparisonFunction.GreaterThan(expression1, expression2, isMatchingCase, matchAction);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsGreaterThanOrEqualTo greaterOrEqual(
-            final Expression expr1, final Expression expr2) {
-        return greaterOrEqual(expr1, expr2,false, MatchAction.ANY);
+    public PropertyIsGreaterThanOrEqualTo greaterOrEqual(final Expression expression1, final Expression expression2) {
+        return greaterOrEqual(expression1, expression2,false, MatchAction.ANY);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsGreaterThanOrEqualTo greaterOrEqual(
-            final Expression expr1, final Expression expr2, final boolean matchCase, final MatchAction matchAction) {
-        return new DefaultPropertyIsGreaterThanOrEqualTo(expr1, expr2, matchCase, matchAction);
+    public PropertyIsGreaterThanOrEqualTo greaterOrEqual(final Expression expression1, final Expression expression2,
+                                                         final boolean isMatchingCase, final MatchAction matchAction)
+    {
+        return new ComparisonFunction.GreaterThanOrEqualTo(expression1, expression2, isMatchingCase, matchAction);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsLessThan less(final Expression expr1, final Expression expr2) {
-        return less(expr1, expr2, false, MatchAction.ANY);
+    public PropertyIsLessThan less(final Expression expression1, final Expression expression2) {
+        return less(expression1, expression2, false, MatchAction.ANY);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsLessThan less(final Expression expr1,
-            final Expression expr2, final boolean matchCase, MatchAction matchAction) {
-        return new DefaultPropertyIsLessThan(expr1, expr2, matchCase, matchAction);
+    public PropertyIsLessThan less(final Expression expression1, final Expression expression2,
+                                   final boolean isMatchingCase, MatchAction matchAction)
+    {
+        return new ComparisonFunction.LessThan(expression1, expression2, isMatchingCase, matchAction);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsLessThanOrEqualTo lessOrEqual(
-            final Expression expr1, final Expression expr2) {
-        return lessOrEqual(expr1, expr2, false, MatchAction.ANY);
+    public PropertyIsLessThanOrEqualTo lessOrEqual(final Expression expression1, final Expression expression2) {
+        return lessOrEqual(expression1, expression2, false, MatchAction.ANY);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsLessThanOrEqualTo lessOrEqual(final Expression expr1,
-            final Expression expr2, final boolean matchCase, final MatchAction matchAction) {
-        return new DefaultPropertyIsLessThanOrEqualTo(expr1, expr2, matchCase, matchAction);
+    public PropertyIsLessThanOrEqualTo lessOrEqual(final Expression expression1, final Expression expression2,
+                                                   final boolean isMatchingCase, final MatchAction matchAction)
+    {
+        return new ComparisonFunction.LessThanOrEqualTo(expression1, expression2, isMatchingCase, matchAction);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsLike like(final Expression expr, final String pattern) {
-        return like(expr, pattern, "*", "?", "\\");
+    public PropertyIsLike like(final Expression expression, final String pattern) {
+        return like(expression, pattern, "*", "?", "\\");
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsLike like(final Expression expr, final String pattern,
-            final String wildcard, final String singleChar, final String escape) {
-        return like(expr,pattern,wildcard,singleChar,escape,false);
+    public PropertyIsLike like(final Expression expression, final String pattern,
+            final String wildcard, final String singleChar, final String escape)
+    {
+        return like(expression,pattern,wildcard,singleChar,escape,false);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsLike like(final Expression expr, final String pattern,
+    public PropertyIsLike like(final Expression expression, final String pattern,
             final String wildcard, final String singleChar,
-            final String escape, final boolean matchCase) {
-        return new DefaultPropertyIsLike(expr, pattern, wildcard, singleChar, escape, matchCase);
+            final String escape, final boolean isMatchingCase)
+    {
+        throw new UnsupportedOperationException("Not supported yet.");
     }
 
     /**
@@ -529,15 +526,14 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public PropertyIsNull isNull(final Expression expression) {
-        ArgumentChecks.ensureNonNull("expression", expression);
-        return new DefaultPropertyIsNull(expression);
+        return new UnaryFunction.IsNull(expression);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsNil isNil(Expression expr) {
+    public PropertyIsNil isNil(Expression expression) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -547,7 +543,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public After after(Expression expr1, Expression expr2) {
+    public After after(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -555,7 +551,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public AnyInteracts anyInteracts(Expression expr1, Expression expr2) {
+    public AnyInteracts anyInteracts(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -563,7 +559,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public Before before(Expression expr1, Expression expr2) {
+    public Before before(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -571,7 +567,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public Begins begins(Expression expr1, Expression expr2) {
+    public Begins begins(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -579,7 +575,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public BegunBy begunBy(Expression expr1, Expression expr2) {
+    public BegunBy begunBy(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -587,7 +583,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public During during(Expression expr1, Expression expr2) {
+    public During during(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -595,7 +591,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public Ends ends(Expression expr1, Expression expr2) {
+    public Ends ends(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -603,7 +599,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public EndedBy endedBy(Expression expr1, Expression expr2) {
+    public EndedBy endedBy(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -611,7 +607,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public Meets meets(Expression expr1, Expression expr2) {
+    public Meets meets(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -619,7 +615,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public MetBy metBy(Expression expr1, Expression expr2) {
+    public MetBy metBy(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -627,7 +623,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public OverlappedBy overlappedBy(Expression expr1, Expression expr2) {
+    public OverlappedBy overlappedBy(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -635,7 +631,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public TContains tcontains(Expression expr1, Expression expr2) {
+    public TContains tcontains(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -643,7 +639,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public TEquals tequals(Expression expr1, Expression expr2) {
+    public TEquals tequals(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -651,7 +647,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public TOverlaps toverlaps(Expression expr1, Expression expr2) {
+    public TOverlaps toverlaps(Expression expression1, Expression expression2) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
@@ -661,32 +657,32 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public Add add(final Expression expr1, final Expression expr2) {
-        return new DefaultAdd(expr1, expr2);
+    public Add add(final Expression expression1, final Expression expression2) {
+        return new ArithmeticFunction.Add(expression1, expression2);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public Divide divide(final Expression expr1, final Expression expr2) {
-        return new DefaultDivide(expr1, expr2);
+    public Divide divide(final Expression expression1, final Expression expression2) {
+        return new ArithmeticFunction.Divide(expression1, expression2);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public Multiply multiply(final Expression expr1, final Expression expr2) {
-        return new DefaultMultiply(expr1, expr2);
+    public Multiply multiply(final Expression expression1, final Expression expression2) {
+        return new ArithmeticFunction.Multiply(expression1, expression2);
     }
 
     /**
      * {@inheritDoc }
      */
     @Override
-    public Subtract subtract(final Expression expr1, final Expression expr2) {
-        return new DefaultSubtract(expr1, expr2);
+    public Subtract subtract(final Expression expression1, final Expression expression2) {
+        return new ArithmeticFunction.Subtract(expression1, expression2);
     }
 
     /**
@@ -702,8 +698,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Literal literal(final Object value) {
-        ArgumentChecks.ensureNonNull("value", value);
-        return new DefaultLiteral<>(value);
+        return new LeafExpression.Literal(value);
     }
 
     /**
@@ -711,7 +706,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Literal literal(final byte value) {
-        return new DefaultLiteral<>(value);
+        return new LeafExpression.Literal(value);
     }
 
     /**
@@ -719,7 +714,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Literal literal(final short value) {
-        return new DefaultLiteral<>(value);
+        return new LeafExpression.Literal(value);
     }
 
     /**
@@ -727,7 +722,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Literal literal(final int value) {
-        return new DefaultLiteral<>(value);
+        return new LeafExpression.Literal(value);
     }
 
     /**
@@ -735,7 +730,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Literal literal(final long value) {
-        return new DefaultLiteral<>(value);
+        return new LeafExpression.Literal(value);
     }
 
     /**
@@ -743,7 +738,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Literal literal(final float value) {
-        return new DefaultLiteral<>(value);
+        return new LeafExpression.Literal(value);
     }
 
     /**
@@ -751,7 +746,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Literal literal(final double value) {
-        return new DefaultLiteral<>(value);
+        return new LeafExpression.Literal(value);
     }
 
     /**
@@ -759,7 +754,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Literal literal(final char value) {
-        return new DefaultLiteral<>(value);
+        return new LeafExpression.Literal(value);
     }
 
     /**
@@ -767,7 +762,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      */
     @Override
     public Literal literal(final boolean value) {
-        return new DefaultLiteral<>(value);
+        return new LeafExpression.Literal(value);
     }
 
     // SORT BY /////////////////////////////////////////////////////////////////
@@ -794,8 +789,7 @@ public class DefaultFilterFactory implements FilterFactory2 {
      * {@inheritDoc }
      */
     @Override
-    public SpatialOperator spatialOperator(final String name,
-            final GeometryOperand[] geometryOperands) {
+    public SpatialOperator spatialOperator(final String name, final GeometryOperand[] geometryOperands) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
 
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultId.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultId.java
deleted file mode 100644
index 204337e..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultId.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Objects;
-import java.util.Set;
-import org.apache.sis.internal.feature.AttributeConvention;
-import org.opengis.feature.Feature;
-import org.opengis.feature.PropertyNotFoundException;
-import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.Id;
-import org.opengis.filter.identity.Identifier;
-
-/**
- * Filter features using a list of predefined ids and discarding those not in the list.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class DefaultId implements Id, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = 1404452049863376235L;
-
-    private final DualKeyMap keys = new DualKeyMap();
-
-    public DefaultId( final Set<? extends Identifier> ids ) {
-        for (Identifier id : ids) {
-            keys.put(id.getID(), id);
-        }
-    }
-
-    @Override
-    public Set<Object> getIDs() {
-        return Collections.unmodifiableSet(keys.keySet());
-    }
-
-    @Override
-    public Set<Identifier> getIdentifiers() {
-        return new HashSet<>(keys.values());
-    }
-
-    @Override
-    public boolean evaluate(Object object) {
-        if (object instanceof Feature) {
-            final Feature f = (Feature) object;
-            try {
-                Object id = f.getPropertyValue(AttributeConvention.IDENTIFIER);
-                if (id == null) {
-                   return false;
-                } else if (id instanceof String) {
-                    return keys.containsKey(id);
-                } else {
-                    //it often happens like in web services that keys are sent as Strings
-                    //but the real type might be different
-                    return keys.containsKey(id) || keys.containsKey(String.valueOf(id));
-                }
-            } catch (PropertyNotFoundException ex) {
-                //normal
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(FilterVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final DefaultId other = (DefaultId) obj;
-        return Objects.equals(this.keys, other.keys);
-    }
-
-    @Override
-    public int hashCode() {
-        return keys.hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return AbstractExpression.toStringTree("Ids", keys.keySet());
-    }
-
-    private static class DualKeyMap extends HashMap<Object,Identifier> {
-
-        @Override
-        public boolean containsValue(final Object value) {
-            if (value instanceof Identifier) {
-                Identifier ident = (Identifier) value;
-                return containsKey(ident.getID());
-            } else {
-                return false;
-            }
-        }
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultLiteral.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultLiteral.java
deleted file mode 100644
index 33a10ea..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultLiteral.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Collections;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ConcurrentHashMap;
-import java.io.Serializable;
-import org.opengis.util.LocalName;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.AttributeType;
-import org.opengis.filter.expression.Literal;
-import org.opengis.filter.expression.ExpressionVisitor;
-import org.apache.sis.feature.DefaultAttributeType;
-import org.apache.sis.util.Classes;
-import org.apache.sis.util.iso.Names;
-
-
-/**
- * A constant, literal value that can be used in expressions.
- * The {@link #evaluate(Object)} method ignore the argument and always returns {@link #getValue()}.
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
- *
- * @param <T>  the literal value type.
- *
- * @since 1.0
- * @module
- */
-final class DefaultLiteral<T> extends AbstractExpression implements Literal, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = 3240145927452086297L;
-
-    /**
-     * The name of attribute types associated with literals.
-     */
-    private static final LocalName NAME = Names.createLocalName(null, null, "Literal");
-
-    /**
-     * A cache of {@link AttributeType} instances for literal classes. Used for avoiding to create many duplicated
-     * instances for the common case where the literal is a very common type like {@link String} or {@link Integer}.
-     */
-    private static final ConcurrentMap<Class<?>, AttributeType<?>> TYPES = new ConcurrentHashMap<>();
-
-    /**
-     * The constant value to be returned by {@link #getValue()}.
-     */
-    private final T value;
-
-    /**
-     * Creates a new literal holding the given constant value.
-     * It is caller responsibility to ensure that the given argument is non-null.
-     */
-    DefaultLiteral(final T value) {
-        this.value = value;
-    }
-
-    /**
-     * Returns the constant value held by this object.
-     */
-    @Override
-    public T getValue() {
-        return value;
-    }
-
-    /**
-     * Returns the constant value held by this object.
-     */
-    @Override
-    public T evaluate(Object ignored) {
-        return value;
-    }
-
-    /**
-     * Returns the type of values produced by this expression.
-     */
-    @Override
-    public AttributeType<?> expectedType(FeatureType ignored) {
-        final Class<?> valueType = value.getClass();
-        AttributeType<?> type = TYPES.get(valueType);
-        if (type == null) {
-            final Class<?> standardType = Classes.getStandardType(valueType);
-            type = TYPES.computeIfAbsent(standardType, DefaultLiteral::newType);
-            if (valueType != standardType) {
-                TYPES.put(valueType, type);
-            }
-        }
-        return type;
-    }
-
-    /**
-     * Invoked when a new attribute type need to be created for the given standard type.
-     */
-    private static <T> AttributeType<T> newType(final Class<T> standardType) {
-        return new DefaultAttributeType<>(Collections.singletonMap(DefaultAttributeType.NAME_KEY, NAME),
-                                          standardType, 1, 1, null, (AttributeType<?>[]) null);
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(final ExpressionVisitor visitor, final Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    /**
-     * Compares this literal with the given object for equality.
-     */
-    @Override
-    public boolean equals(final Object obj) {
-        return (obj instanceof Literal) && value.equals(((DefaultLiteral) obj).value);
-    }
-
-    /**
-     * Returns a hash-code value for this literal.
-     */
-    @Override
-    public int hashCode() {
-        return value.hashCode() ^ (int) serialVersionUID;
-    }
-
-    /**
-     * Returns a string representation of this literal.
-     */
-    @Override
-    public String toString() {
-        return value.toString();
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultMultiply.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultMultiply.java
deleted file mode 100644
index 1b088a4..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultMultiply.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Collections;
-import org.apache.sis.feature.DefaultAttributeType;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyType;
-import org.opengis.filter.expression.Expression;
-import org.opengis.filter.expression.ExpressionVisitor;
-import org.opengis.filter.expression.Multiply;
-
-/**
- * Multiply expression.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class DefaultMultiply extends AbstractBinaryExpression implements Multiply {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = 2082613762722323696L;
-
-    private static final AttributeType<Number> EXPECTED_TYPE = new DefaultAttributeType<>(
-            Collections.singletonMap(DefaultAttributeType.NAME_KEY, NAME),
-                                          Number.class, 1, 1, null, (AttributeType<?>[]) null);
-
-    public DefaultMultiply(Expression expressoin1, Expression expression2) {
-        super(expressoin1, expression2);
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(ExpressionVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    protected char symbol() {
-        return '*';
-    }
-
-    @Override
-    public Object evaluate(Object object) {
-        final Double val1 = expression1.evaluate(object, Double.class);
-        final Double val2 = expression2.evaluate(object, Double.class);
-
-        if (val1 == null || val2 == null) {
-            return null;
-        }
-
-        return val1 * val2;
-    }
-
-    @Override
-    public PropertyType expectedType(FeatureType type) {
-        return EXPECTED_TYPE;
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultNot.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultNot.java
deleted file mode 100644
index 88fc8e4..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultNot.java
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import java.util.Arrays;
-import java.util.Objects;
-import org.apache.sis.util.ArgumentChecks;
-import org.opengis.filter.Filter;
-import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.Not;
-
-/**
- * Negation filter.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class DefaultNot implements Not, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -1216799270370223357L;
-
-    private final Filter filter;
-
-    public DefaultNot(Filter filter) {
-        ArgumentChecks.ensureNonNull("filter", filter);
-        this.filter = filter;
-    }
-
-    @Override
-    public Filter getFilter() {
-        return filter;
-    }
-
-    @Override
-    public boolean evaluate(Object object) {
-        return !filter.evaluate(object);
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(FilterVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final DefaultNot other = (DefaultNot) obj;
-        return Objects.equals(this.filter, other.filter);
-    }
-
-    @Override
-    public int hashCode() {
-        return 32 * filter.hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return AbstractExpression.toStringTree("Not", Arrays.asList(filter));
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultObjectId.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultObjectId.java
new file mode 100644
index 0000000..512071d
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultObjectId.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.io.Serializable;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.opengis.feature.Feature;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.filter.identity.FeatureId;
+import org.opengis.filter.identity.GmlObjectId;
+
+
+/**
+ * Default implementation of a few interfaces from the {@link org.opengis.filter.identity} package.
+ * Those objects are used for identifying GML objects or other kind of objects.
+ *
+ * @deprecated the purpose of {@link org.opengis.filter.identity} is questionable.
+ *             See <a href="https://github.com/opengeospatial/geoapi/issues/32">GeoAPI issue #32</a>.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+@Deprecated
+final class DefaultObjectId implements FeatureId, GmlObjectId, Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -2877500277700165269L;
+
+    /**
+     * The identifier.
+     */
+    private final String identifier;
+
+    /**
+     * Creates a new identifier.
+     */
+    DefaultObjectId(final String id) {
+        ArgumentChecks.ensureNonNull("id", id);
+        identifier = id;
+    }
+
+    /**
+     * Returns the identifier specified at construction time.
+     */
+    @Override
+    public String getID() {
+        return identifier;
+    }
+
+    /**
+     * Returns {@code true} if the given object is a feature with the identifier expected by this class.
+     */
+    @Override
+    public boolean matches(final Object feature) {
+        if (feature instanceof Feature) try {
+            Object id = ((Feature) feature).getPropertyValue(AttributeConvention.IDENTIFIER);
+            return identifier.equals(String.valueOf(id));
+        } catch (PropertyNotFoundException ex) {
+            // Feature does not contain the identifier property.
+        }
+        return false;
+    }
+
+    /**
+     * Returns {@code true} if the given object is an identifier with equals {@link String} code than this object.
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj instanceof DefaultObjectId) {
+            return identifier.equals(((DefaultObjectId) obj).identifier);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code value based on the identifier specified at construction time.
+     */
+    @Override
+    public int hashCode() {
+        return identifier.hashCode() ^ (int) serialVersionUID;
+    }
+
+    /**
+     * Returns a string representation of this identifier.
+     */
+    @Override
+    public String toString() {
+        return "Id:".concat(identifier);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultOr.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultOr.java
deleted file mode 100644
index b327d16..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultOr.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import org.opengis.filter.Filter;
-import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.Or;
-
-/**
- * Binary logic filter OR.
- * At least one child filter must be true to pass.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class DefaultOr implements Or, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = 8789522191433875591L;
-
-    private final List<Filter> filters;
-
-    public DefaultOr(List<Filter> filters) {
-        if (filters.size() < 2) throw new IllegalArgumentException("At least two filters are requiered");
-        this.filters = Collections.unmodifiableList(filters);
-    }
-
-    /**
-     * {@inheritDoc}
-     * @return list of at least two {@code Filter}.
-     */
-    @Override
-    public List<Filter> getChildren() {
-        return filters;
-    }
-
-    @Override
-    public boolean evaluate(Object object) {
-        for (Filter filter : filters) {
-            if (filter.evaluate(object)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(FilterVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        if (this == obj) {
-            return true;
-        }
-        if (obj == null) {
-            return false;
-        }
-        if (getClass() != obj.getClass()) {
-            return false;
-        }
-        final DefaultOr other = (DefaultOr) obj;
-        return Objects.equals(this.filters, other.filters);
-    }
-
-    @Override
-    public int hashCode() {
-        return 47 * filters.hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return AbstractExpression.toStringTree("Or", filters);
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java
deleted file mode 100644
index 2a4e78e..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Objects;
-import org.apache.sis.util.Numbers;
-import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.MatchAction;
-import org.opengis.filter.PropertyIsEqualTo;
-import org.opengis.filter.expression.Expression;
-
-
-/**
- * Filter operator that compares that its two sub-expressions are equal to each other.
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-final class DefaultPropertyIsEqualTo extends AbstractComparisonOperator implements PropertyIsEqualTo {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -5783347523815670017L;
-
-    /**
-     * Creates a new comparison operator.
-     * It is caller responsibility to ensure that no argument is null.
-     */
-    DefaultPropertyIsEqualTo(Expression expression1, Expression expression2, boolean matchCase, MatchAction matchAction) {
-        super(expression1, expression2, matchCase, matchAction);
-    }
-
-    /**
-     * Returns the mathematical symbol for this comparison operator.
-     */
-    @Override
-    protected char symbol() {
-        return '=';
-    }
-
-    /**
-     * Determines if the test represented by this filter passed.
-     *
-     * @todo Use locale-sensitive {@link java.text.Collator} for string comparisons.
-     */
-    @Override
-    public boolean evaluate(Object object) {
-        final Object r1 = expression1.evaluate(object);
-        final Object r2 = expression2.evaluate(object);
-        if (Objects.equals(r1, r2)) {
-            return true;
-        } else if (r1 instanceof Number && r2 instanceof Number) {
-            @SuppressWarnings("unchecked") final Class<? extends Number> c1 = (Class<? extends Number>) r1.getClass();
-            @SuppressWarnings("unchecked") final Class<? extends Number> c2 = (Class<? extends Number>) r2.getClass();
-            if (c1 != c2) {
-                final Class<? extends Number> c = Numbers.widestClass(c1, c2);
-                return Numbers.cast((Number) r1, c).equals(
-                       Numbers.cast((Number) r2, c));
-            }
-        } else if (r1 instanceof CharSequence && r2 instanceof CharSequence) {
-            final String s1 = r1.toString();
-            final String s2 = r2.toString();
-            if (!matchCase) {
-                return s1.equalsIgnoreCase(s2);
-            } else if (r1 != s1 || r2 != s2) {
-                return s1.equals(s2);
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(FilterVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsGreaterThan.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsGreaterThan.java
deleted file mode 100644
index d950129..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsGreaterThan.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.MatchAction;
-import org.opengis.filter.PropertyIsGreaterThan;
-import org.opengis.filter.expression.Expression;
-
-/**
- * Immutable "is greater than" filter.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-final class DefaultPropertyIsGreaterThan extends AbstractBinaryComparisonOperator implements PropertyIsGreaterThan {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -1944045883935206615L;
-
-    public DefaultPropertyIsGreaterThan(final Expression left, final Expression right, final boolean match, final MatchAction matchAction) {
-        super(left,right,match,matchAction);
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public boolean evaluateOne(Object l, Object r) {
-        final Integer v = compare(l,r);
-        return (v == null) ? false : (v > 0) ;
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public Object accept(final FilterVisitor visitor, final Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    protected char symbol() {
-        return '>';
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsGreaterThanOrEqualTo.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsGreaterThanOrEqualTo.java
deleted file mode 100644
index 50d6ed3..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsGreaterThanOrEqualTo.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.MatchAction;
-import org.opengis.filter.PropertyIsGreaterThanOrEqualTo;
-import org.opengis.filter.expression.Expression;
-
-/**
- * Immutable "is greater than or equal" fitler.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-final class DefaultPropertyIsGreaterThanOrEqualTo extends AbstractBinaryComparisonOperator implements PropertyIsGreaterThanOrEqualTo {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = 1723032440128570732L;
-
-    public DefaultPropertyIsGreaterThanOrEqualTo(final Expression left, final Expression right, final boolean match, final MatchAction matchAction) {
-        super(left,right,match,matchAction);
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public boolean evaluateOne(Object l, Object r) {
-        final Integer v = compare(l,r);
-        return (v == null) ? false : (v >= 0) ;
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public Object accept(final FilterVisitor visitor, final Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    protected char symbol() {
-        return '≥';
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsLessThan.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsLessThan.java
deleted file mode 100644
index 44ebb96..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsLessThan.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.MatchAction;
-import org.opengis.filter.PropertyIsLessThan;
-import org.opengis.filter.expression.Expression;
-
-/**
- * Immutable "is less" filter.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-final class DefaultPropertyIsLessThan extends AbstractBinaryComparisonOperator implements PropertyIsLessThan {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -4576447434041801535L;
-
-    public DefaultPropertyIsLessThan(final Expression left, final Expression right, final boolean match, final MatchAction matchAction) {
-        super(left,right,match,matchAction);
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public boolean evaluateOne(Object l, Object r) {
-        final Integer v = compare(l,r);
-        return (v == null) ? false : (v < 0) ;
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public Object accept(final FilterVisitor visitor, final Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    protected char symbol() {
-        return '<';
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsLessThanOrEqualTo.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsLessThanOrEqualTo.java
deleted file mode 100644
index 753ea3f..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsLessThanOrEqualTo.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.MatchAction;
-import org.opengis.filter.PropertyIsLessThanOrEqualTo;
-import org.opengis.filter.expression.Expression;
-
-/**
- * Immutable "is less or equal" filter.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-final class DefaultPropertyIsLessThanOrEqualTo extends AbstractBinaryComparisonOperator implements PropertyIsLessThanOrEqualTo {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -5046947167513679010L;
-
-    public DefaultPropertyIsLessThanOrEqualTo(final Expression left, final Expression right, final boolean match, final MatchAction matchAction) {
-        super(left,right,match,matchAction);
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public boolean evaluateOne(Object l, Object r) {
-        final Integer v = compare(l,r);
-        return (v == null) ? false : (v <= 0) ;
-    }
-
-    /**
-     * {@inheritDoc }
-     */
-    @Override
-    public Object accept(final FilterVisitor visitor, final Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    protected char symbol() {
-        return '≤';
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsLike.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsLike.java
deleted file mode 100644
index 16e328f..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsLike.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
-import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.PropertyIsLike;
-import org.opengis.filter.expression.Expression;
-
-/**
- * TODO
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-public class DefaultPropertyIsLike implements PropertyIsLike {
-
-    private final Expression expression;
-    private final String pattern;
-    private final String wildcardMulti;
-    private final String wildcardSingle;
-    private final String escape;
-    private final boolean matchingCase;
-
-    public DefaultPropertyIsLike(final Expression expression, final String pattern, final String wildcardMulti,
-            final String wildcardSingle, final String escape, final boolean matchCase) {
-        ensureNonNull("expression", expression);
-
-        this.expression = expression;
-        this.pattern = pattern;
-        this.wildcardMulti = wildcardMulti;
-        this.wildcardSingle = wildcardSingle;
-        this.escape = escape;
-        this.matchingCase = matchCase;
-    }
-
-    @Override
-    public Expression getExpression() {
-        return expression;
-    }
-
-    @Override
-    public String getLiteral() {
-        return pattern;
-    }
-
-    @Override
-    public String getWildCard() {
-        return wildcardMulti;
-    }
-
-    @Override
-    public String getSingleChar() {
-        return wildcardSingle;
-    }
-
-    @Override
-    public String getEscape() {
-        return escape;
-    }
-
-    @Override
-    public boolean isMatchingCase() {
-        return matchingCase;
-    }
-
-    @Override
-    public boolean evaluate(Object object) {
-        throw new UnsupportedOperationException("Not supported yet.");
-    }
-
-    @Override
-    public Object accept(FilterVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsNull.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsNull.java
deleted file mode 100644
index 9c2a238..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsNull.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.io.Serializable;
-import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.PropertyIsNull;
-import org.opengis.filter.expression.Expression;
-
-
-/**
- * Filter operator that checks if an expression's value is {@code null}.  A {@code null}
- * is equivalent to no value present. The value 0 is a valid value and is not considered
- * {@code null}.
- *
- * @author  Johann Sorel (Geomatys)
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-final class DefaultPropertyIsNull extends AbstractUnaryOperator implements PropertyIsNull, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = 3942075458551232678L;
-
-    /**
-     * Creates a new operator.
-     * It is caller responsibility to ensure that no argument is null.
-     */
-    DefaultPropertyIsNull(final Expression expression) {
-        super(expression);
-    }
-
-    /**
-     * Returns the null symbol, to be used in string representation.
-     */
-    @Override
-    protected char symbol() {
-        return '∅';
-    }
-
-    /**
-     * Returns {@code true} if the given value evaluates to {@code null}.
-     */
-    @Override
-    public boolean evaluate(final Object object) {
-        return expression.evaluate(object) == null;
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(final FilterVisitor visitor, final Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyName.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyName.java
deleted file mode 100644
index 1d1626a..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyName.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Map;
-import java.io.Serializable;
-import org.apache.sis.util.Classes;
-import org.apache.sis.util.resources.Errors;
-import org.apache.sis.feature.DefaultAssociationRole;
-
-import static java.util.Collections.singletonMap;
-
-// Branch-dependent imports
-import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.IdentifiedType;
-import org.opengis.feature.Operation;
-import org.opengis.feature.PropertyType;
-import org.opengis.feature.PropertyNotFoundException;
-import org.opengis.filter.expression.ExpressionVisitor;
-import org.opengis.filter.expression.PropertyName;
-
-
-/**
- * Expression whose value is computed by retrieving the value indicated by the provided name.
- * A property name does not store any value; it acts as an indirection to a property value of
- * the evaluated feature.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-final class DefaultPropertyName extends AbstractExpression implements PropertyName, Serializable {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -8474562134021521300L;
-
-    /**
-     * Name of the property from which to retrieve the value.
-     */
-    private final String name;
-
-    /**
-     * Creates a new expression retrieving values from a property of the given name.
-     * It is caller responsibility to ensure that the given name is non-null.
-     *
-     * @param  name  name of the property (usually a feature attribute).
-     */
-    DefaultPropertyName(final String name) {
-        this.name = name;
-    }
-
-    /**
-     * Returns the name of the property whose value will be returned by the {@link #evaluate evaluate} method.
-     */
-    @Override
-    public String getPropertyName() {
-        return name;
-    }
-
-    /**
-     * Returns the value of the property of the given name.
-     * The {@code candidate} object can be any of the following type:
-     *
-     * <ul>
-     *   <li>A {@link Feature}, in which case {@link Feature#getPropertyValue(String)} will be invoked.</li>
-     *   <li>A {@link Map}, in which case {@link Map#get(Object)} will be invoked.</li>
-     * </ul>
-     *
-     * If no value is found for the given property, then this method returns {@code null}.
-     */
-    @Override
-    public Object evaluate(final Object candidate) {
-        if (candidate instanceof Feature) {
-            try {
-                return ((Feature) candidate).getPropertyValue(name);
-            } catch (PropertyNotFoundException ex) {
-                // Null will be returned below.
-                // TODO: report a warning somewhere?
-            }
-        } else if (candidate instanceof Map<?,?>) {
-            return ((Map<?,?>) candidate).get(name);
-        }
-        return null;
-    }
-
-    /**
-     * Returns the expected type of values produced by this expression when a feature of the given type is evaluated.
-     *
-     * @throws IllegalArgumentException if this method can not determine the property type for the given feature type.
-     */
-    @Override
-    public PropertyType expectedType(final FeatureType type) {
-        PropertyType propertyType = type.getProperty(name);         // May throw IllegalArgumentException.
-        while (propertyType instanceof Operation) {
-            final IdentifiedType it = ((Operation) propertyType).getResult();
-            if (it instanceof PropertyType) {
-                propertyType = (PropertyType) it;
-            } else if (it instanceof FeatureType) {
-                propertyType = new DefaultAssociationRole(singletonMap(DefaultAssociationRole.NAME_KEY, name), type, 1, 1);
-            } else {
-                throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalPropertyValueClass_3,
-                            name, PropertyType.class, Classes.getStandardType(Classes.getClass(it))));
-            }
-        }
-        return propertyType;
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(final ExpressionVisitor visitor, final Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    /**
-     * Returns a hash-code value for this expression.
-     */
-    @Override
-    public boolean equals(final Object obj) {
-        return (obj instanceof DefaultPropertyName) && name.equals(((DefaultPropertyName) obj).name);
-    }
-
-    /**
-     * Returns a hash-code value for this expression.
-     */
-    @Override
-    public int hashCode() {
-        return name.hashCode() ^ (int) serialVersionUID;
-    }
-
-    /**
-     * Returns a string representation of this expression.
-     */
-    @Override
-    public String toString() {
-        return '{' + name + '}';
-    }
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultSubtract.java b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultSubtract.java
deleted file mode 100644
index f76d6b2..0000000
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultSubtract.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Collections;
-import org.apache.sis.feature.DefaultAttributeType;
-import org.opengis.feature.AttributeType;
-import org.opengis.feature.FeatureType;
-import org.opengis.feature.PropertyType;
-import org.opengis.filter.expression.Expression;
-import org.opengis.filter.expression.ExpressionVisitor;
-import org.opengis.filter.expression.Subtract;
-
-/**
- * Subtraction expression.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-final class DefaultSubtract extends AbstractBinaryExpression implements Subtract {
-
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = -6691179262852420992L;
-
-    private static final AttributeType<Number> EXPECTED_TYPE = new DefaultAttributeType<>(
-            Collections.singletonMap(DefaultAttributeType.NAME_KEY, NAME),
-                                          Number.class, 1, 1, null, (AttributeType<?>[]) null);
-
-    public DefaultSubtract(Expression expressoin1, Expression expression2) {
-        super(expressoin1, expression2);
-    }
-
-    /**
-     * Accepts a visitor.
-     */
-    @Override
-    public Object accept(ExpressionVisitor visitor, Object extraData) {
-        return visitor.visit(this, extraData);
-    }
-
-    @Override
-    protected char symbol() {
-        return '-';
-    }
-
-    @Override
-    public Object evaluate(Object object) {
-        final Double val1 = expression1.evaluate(object, Double.class);
-        final Double val2 = expression2.evaluate(object, Double.class);
-
-        if (val1 == null || val2 == null) {
-            return null;
-        }
-
-        return val1 - val2;
-    }
-
-    @Override
-    public PropertyType expectedType(FeatureType type) {
-        return EXPECTED_TYPE;
-    }
-
-}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/FilterByIdentifier.java b/core/sis-feature/src/main/java/org/apache/sis/filter/FilterByIdentifier.java
new file mode 100644
index 0000000..2ba3a83
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/FilterByIdentifier.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.sis.util.collection.Containers;
+import org.apache.sis.internal.feature.AttributeConvention;
+
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.filter.FilterVisitor;
+import org.opengis.filter.Id;
+import org.opengis.filter.identity.Identifier;
+
+
+/**
+ * Filter features using a set of predefined identifiers and discarding features
+ * whose identifier is not in the set.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+final class FilterByIdentifier extends Node implements Id {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 1404452049863376235L;
+
+    /**
+     * The identifiers of features to retain. Filtering will use the keys. This map contains also
+     * the same identifiers as the original {@link Identifier} objects given at construction time,
+     * but those values are not used by this class.
+     */
+    private final Map<Object,Identifier> identifiers;
+
+    /**
+     * Creates a new filter using the given identifiers.
+     */
+    FilterByIdentifier(final Collection<? extends Identifier> ids) {
+        identifiers = new HashMap<>(Containers.hashMapCapacity(ids.size()));
+        for (Identifier id : ids) {
+            identifiers.put(id.getID(), id);
+        }
+    }
+
+    /**
+     * Returns a name identifying this kind of filter.
+     */
+    @Override
+    protected String name() {
+        return "Id";
+    }
+
+    /**
+     * Returns the identifiers specified at construction time. This is used for {@link #toString()},
+     * {@link #hashCode()} and {@link #equals(Object)} implementations. Since all the keys in the
+     * {@link #identifiers} map were derived from the values, comparing those values is sufficient
+     * for determining if two {@code FilterByIdentifier} instances are equal.
+     */
+    @Override
+    protected Collection<?> getChildren() {
+        // Can not return identifiers.values() directly because that collection does not implement equals/hashCode.
+        return getIdentifiers();
+    }
+
+    /**
+     * Returns the identifiers of feature instances to accept.
+     */
+    @Override
+    public Set<Object> getIDs() {
+        return Collections.unmodifiableSet(identifiers.keySet());
+    }
+
+    /**
+     * Same identifiers than {@link #getIDs()} but as the instances specified at construction time.
+     * This is not used by this class.
+     */
+    @Override
+    public Set<Identifier> getIdentifiers() {
+        return new HashSet<>(identifiers.values());
+    }
+
+    /**
+     * Returns {@code true} if the given object is a {@link Feature} instance and its identifier
+     * is one of the identifier specified at {@code FilterByIdentifier} construction time.
+     */
+    @Override
+    public boolean evaluate(Object object) {
+        if (object instanceof Feature) try {
+            final Object id = ((Feature) object).getPropertyValue(AttributeConvention.IDENTIFIER);
+            if (identifiers.containsKey(id)) {
+                return true;
+            }
+            if (id != null && !(id instanceof String)) {
+                /*
+                 * Sometime web services specify the identifiers to use for filtering as Strings
+                 * while the types stored in the feature instances is different.
+                 */
+                return identifiers.containsKey(id.toString());
+            }
+        } catch (PropertyNotFoundException ex) {
+            // No identifier property. This is okay.
+        }
+        return false;
+    }
+
+    /**
+     * Implementation of the visitor pattern.
+     */
+    @Override
+    public Object accept(FilterVisitor visitor, Object extraData) {
+        return visitor.visit(this, extraData);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java b/core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java
new file mode 100644
index 0000000..42aec25
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java
@@ -0,0 +1,254 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.Map;
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.iso.Names;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ObjectConverters;
+import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.util.collection.WeakValueHashMap;
+import org.apache.sis.internal.feature.FeatureExpression;
+import org.apache.sis.feature.DefaultAssociationRole;
+import org.apache.sis.util.resources.Errors;
+
+// Branch-dependent imports
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.IdentifiedType;
+import org.opengis.feature.Operation;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.filter.expression.Expression;
+import org.opengis.filter.expression.ExpressionVisitor;
+
+
+/**
+ * Expressions that do not depend on any other expression.
+ * Those expression may read value from a feature property, or return a constant value.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+abstract class LeafExpression extends Node implements Expression, FeatureExpression {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 4262341851590811918L;
+
+    /**
+     * Creates a new property reader.
+     */
+    LeafExpression() {
+    }
+
+    /**
+     * Evaluates the expression for producing a result of the given type.
+     * If this method can not produce a value of the given type, then it returns {@code null}.
+     * This implementation evaluates the expression {@linkplain #evaluate(Object) in the default way},
+     * then tries to convert the result to the target type.
+     *
+     * @param  feature  to feature to evaluate with this expression.
+     * @param  target   the desired type for the expression result.
+     * @return the result, or {@code null} if it can not be of the specified type.
+     */
+    @Override
+    public final <T> T evaluate(final Object feature, final Class<T> target) {
+        ArgumentChecks.ensureNonNull("target", target);
+        final Object value = evaluate(feature);
+        try {
+            return ObjectConverters.convert(value, target);
+        } catch (UnconvertibleObjectException e) {
+            warning("evaluate", e);
+            return null;                    // As per method contract.
+        }
+    }
+
+
+
+
+    /**
+     * Expression whose value is computed by retrieving the value indicated by the provided name.
+     * A property name does not store any value; it acts as an indirection to a property value of
+     * the evaluated feature.
+     */
+    static final class Property extends LeafExpression implements org.opengis.filter.expression.PropertyName {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 3417789380239058201L;
+
+        /** Name of the property from which to retrieve the value. */
+        private final String name;
+
+        /** Creates a new expression retrieving values from a property of the given name. */
+        Property(final String name) {
+            ArgumentChecks.ensureNonNull("name", name);
+            this.name = name;
+        }
+
+        /** Identification of this expression. */
+        @Override protected String name() {
+            return "PropertyName";
+        }
+
+        /** For {@link #toString()}, {@link #hashCode()} and {@link #equals(Object)} implementations. */
+        @Override protected Collection<?> getChildren() {
+            return Collections.singleton(name);
+        }
+
+        /** Returns the name of the property whose value will be returned by the {@link #evaluate(Object)} method. */
+        @Override public String getPropertyName() {
+            return name;
+        }
+
+        /**
+         * Returns the value of the property of the given name.
+         * The {@code candidate} object can be any of the following type:
+         *
+         * <ul>
+         *   <li>A {@link Feature}, in which case {@link Feature#getPropertyValue(String)} will be invoked.</li>
+         *   <li>A {@link Map}, in which case {@link Map#get(Object)} will be invoked.</li>
+         * </ul>
+         *
+         * If no value is found for the given property, then this method returns {@code null}.
+         */
+        @Override
+        public Object evaluate(final Object candidate) {
+            if (candidate instanceof Feature) try {
+                return ((Feature) candidate).getPropertyValue(name);
+            } catch (PropertyNotFoundException ex) {
+                warning("evaluate", ex);
+                // Null will be returned below.
+            } else if (candidate instanceof Map<?,?>) {
+                return ((Map<?,?>) candidate).get(name);
+            }
+            return null;
+        }
+
+        /**
+         * Returns the expected type of values produced by this expression when a feature of the given type is evaluated.
+         *
+         * @throws IllegalArgumentException if this method can not determine the property type for the given feature type.
+         */
+        @Override
+        public PropertyType expectedType(final FeatureType type) {
+            PropertyType propertyType = type.getProperty(name);         // May throw IllegalArgumentException.
+            while (propertyType instanceof Operation) {
+                final IdentifiedType it = ((Operation) propertyType).getResult();
+                if (it instanceof PropertyType) {
+                    propertyType = (PropertyType) it;
+                } else if (it instanceof FeatureType) {
+                    return new DefaultAssociationRole(Collections.singletonMap(DefaultAssociationRole.NAME_KEY, name), type, 1, 1);
+                } else {
+                    throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalPropertyValueClass_3,
+                                name, PropertyType.class, Classes.getStandardType(Classes.getClass(it))));
+                }
+            }
+            return propertyType;
+        }
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(final ExpressionVisitor visitor, final Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+
+
+
+
+    /**
+     * A constant, literal value that can be used in expressions.
+     * The {@link #evaluate(Object)} method ignores the argument and always returns {@link #getValue()}.
+     */
+    static final class Literal extends LeafExpression implements org.opengis.filter.expression.Literal {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = -8383113218490957822L;
+
+        /** The constant value to be returned by {@link #getValue()}. */
+        private final Object value;
+
+        /** Creates a new literal holding the given constant value. */
+        Literal(final Object value) {
+            ArgumentChecks.ensureNonNull("value", value);
+            this.value = value;
+        }
+
+        /** Identification of this expression. */
+        @Override protected String name() {
+            return "Literal";
+        }
+
+        /** For {@link #toString()}, {@link #hashCode()} and {@link #equals(Object)} implementations. */
+        @Override protected Collection<?> getChildren() {
+            return Collections.singleton(value);
+        }
+
+        /** Returns the constant value held by this object. */
+        @Override public Object getValue() {
+            return value;
+        }
+
+        /** Expression evaluation, which just returns the constant value. */
+        @Override public Object evaluate(Object ignored) {
+            return value;
+        }
+
+        /**
+         * Returns the type of values returned by {@link #evaluate(Object)},
+         * wrapped in an {@code AttributeType} named "Literal".
+         */
+        @Override
+        public AttributeType<?> expectedType(FeatureType ignored) {
+            final Class<?> valueType = value.getClass();
+            AttributeType<?> type = TYPES.get(valueType);
+            if (type == null) {
+                final Class<?> standardType = Classes.getStandardType(valueType);
+                type = TYPES.computeIfAbsent(standardType, Literal::newType);
+                if (valueType != standardType) {
+                    TYPES.put(valueType, type);
+                }
+            }
+            return type;
+        }
+
+        /**
+         * A cache of {@link AttributeType} instances for literal classes. Used for avoiding to create
+         * duplicated instances when the literal is a common type like {@link String} or {@link Integer}.
+         */
+        @SuppressWarnings("unchecked")
+        private static final WeakValueHashMap<Class<?>, AttributeType<?>> TYPES = new WeakValueHashMap<>((Class) Class.class);
+
+        /**
+         * Invoked when a new attribute type need to be created for the given standard type.
+         * The given standard type should be a GeoAPI interface, not the implementation class.
+         */
+        private static <T> AttributeType<T> newType(final Class<T> standardType) {
+            return createType(standardType, Names.createLocalName(null, null, "Literal"));
+        }
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(final ExpressionVisitor visitor, final Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFunction.java b/core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFunction.java
new file mode 100644
index 0000000..bdac096
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFunction.java
@@ -0,0 +1,158 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Collection;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.internal.util.UnmodifiableArrayList;
+
+// Branch-dependent imports
+import org.opengis.filter.Filter;
+import org.opengis.filter.FilterVisitor;
+
+
+/**
+ * Logical filter (AND, OR) using an arbitrary number of filters.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+abstract class LogicalFunction extends Node {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 3696645262873257479L;
+
+    /**
+     * The filter on which to apply the logical operator.
+     */
+    protected final Filter[] filters;
+
+    /**
+     * Creates a new logical function applied on the given filters.
+     */
+    protected LogicalFunction(final Collection<? extends Filter> f) {
+        ArgumentChecks.ensureNonNull("filters", f);
+        filters = f.toArray(new Filter[f.size()]);
+        for (int i=0; i<filters.length; i++) {
+            ArgumentChecks.ensureNonNullElement("filters", i, filters[i]);
+        }
+        ArgumentChecks.ensureSizeBetween("filters", 2, Integer.MAX_VALUE, filters.length);
+    }
+
+    /**
+     * Returns a list containing all of the child filters of this object.
+     * This list will contain at least two elements.
+     */
+    @Override
+    public final List<Filter> getChildren() {
+        return UnmodifiableArrayList.wrap(filters);
+    }
+
+    /**
+     * Returns a hash code value for this filter.
+     */
+    @Override
+    public final int hashCode() {
+        return getClass().hashCode() ^ Arrays.hashCode(filters);
+    }
+
+    /**
+     * Compares this filter with the given object for equality.
+     */
+    @Override
+    public final boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj != null && obj.getClass() == getClass()) {
+            return Arrays.equals(filters, ((LogicalFunction) obj).filters);
+        }
+        return false;
+    }
+
+
+    /**
+     * The "And" operation (⋀).
+     */
+    static final class And extends LogicalFunction implements org.opengis.filter.And {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 152892064260384713L;
+
+        /** Creates a new expression for the given filters. */
+        And(final Collection<? extends Filter> filters) {
+            super(filters);
+        }
+
+        /** Returns a name for this filter. */
+        @Override protected String name() {return "And";}
+        @Override protected char symbol() {return filters.length <= 2 ? '∧' : '⋀';}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(FilterVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+
+        /** Executes the logical operation. */
+        @Override public boolean evaluate(final Object object) {
+            for (final Filter filter : filters) {
+                if (!filter.evaluate(object)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+
+
+    /**
+     * The "Or" operation (⋁).
+     */
+    static final class Or extends LogicalFunction implements org.opengis.filter.Or {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 3805785720811330282L;
+
+        /** Creates a new expression for the given filters. */
+        Or(final Collection<? extends Filter> filters) {
+            super(filters);
+        }
+
+        /** Returns a name for this filter. */
+        @Override protected String name() {return "Or";}
+        @Override protected char symbol() {return filters.length <= 2 ? '∨' : '⋁';}
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(FilterVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+
+        /** Executes the logical operation. */
+        @Override public boolean evaluate(final Object object) {
+            for (Filter filter : filters) {
+                if (filter.evaluate(object)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/Node.java b/core/sis-feature/src/main/java/org/apache/sis/filter/Node.java
new file mode 100644
index 0000000..f999f9a
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/Node.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.Map;
+import java.util.IdentityHashMap;
+import java.util.Collection;
+import java.io.Serializable;
+import java.util.Collections;
+import org.apache.sis.feature.DefaultAttributeType;
+import org.apache.sis.util.collection.DefaultTreeTable;
+import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.internal.system.Loggers;
+
+// Branch-dependent imports
+import org.opengis.feature.AttributeType;
+import org.opengis.filter.BinaryLogicOperator;
+
+
+/**
+ * Base class of Apache SIS implementation of OGC expressions, comparators or filters.
+ * {@code Node} instances are associated together in a tree, which can be formatted
+ * by {@link #toString()}.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+abstract class Node implements Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = -749201100175374658L;
+
+    /**
+     * Creates a new expression, operator or filter.
+     */
+    protected Node() {
+    }
+
+    /**
+     * Creates an attribute type for values of the given type and name.
+     * The attribute is mandatory, unbounded and has no default value.
+     *
+     * @param  type  type of values in the attribute.
+     * @param  name  name of the attribute to create.
+     * @return an attribute of the given type and name.
+     */
+    static <T> AttributeType<T> createType(final Class<T> type, final Object name) {
+        return new DefaultAttributeType<>(Collections.singletonMap(DefaultAttributeType.NAME_KEY, name),
+                                          type, 1, 1, null, (AttributeType<?>[]) null);
+    }
+
+    /**
+     * Returns the mathematical symbol for this binary function.
+     * For comparison operators, the symbol should be one of {@literal < > ≤ ≥ = ≠}.
+     * For arithmetic operators, the symbol should be one of {@literal + − × ÷}.
+     *
+     * @return the mathematical symbol, or 0 if none.
+     */
+    protected char symbol() {
+        return (char) 0;
+    }
+
+    /**
+     * Returns a name or symbol for this node. This is used for information purpose only,
+     * for example in order to build a string representation.
+     *
+     * @return the name of this node.
+     */
+    protected abstract String name();
+
+    /**
+     * Returns the children of this node, or an empty collection if none. This is used for
+     * information purpose only, for example in order to build a string representation.
+     *
+     * <p>The name of this method is the same as {@link BinaryLogicOperator#getChildren()}
+     * in order to have only one method to override.</p>
+     *
+     * @return the children of this node, or an empty collection if none.
+     */
+    protected abstract Collection<?> getChildren();
+
+    /**
+     * Builds a tree representation of this node, including all children. This method expects
+     * an initially empty node, which will be set to the {@linkplain #name()} of this node.
+     * Then all children will be appended recursively, with a check against cyclic graph.
+     *
+     * @param  root     where to create a tree representation of this node.
+     * @param  visited  nodes already visited. This method will write in this map.
+     */
+    private void toTree(final TreeTable.Node root, final Map<Object,Boolean> visited) {
+        root.setValue(TableColumn.VALUE, name());
+        for (final Object child : getChildren()) {
+            final TreeTable.Node node = root.newChild();
+            final String value;
+            if (child instanceof Node) {
+                if (visited.putIfAbsent(child, Boolean.TRUE) == null) {
+                    ((Node) child).toTree(node, visited);
+                    continue;
+                } else {
+                    value = Vocabulary.format(Vocabulary.Keys.CycleOmitted);
+                }
+            } else {
+                value = String.valueOf(child);
+            }
+            node.setValue(TableColumn.VALUE, value);
+        }
+    }
+
+    /**
+     * Returns a string representation of this node. This representation can be printed
+     * to the {@linkplain System#out standard output stream} (for example) if it uses a
+     * monospaced font and supports Unicode.
+     *
+     * @return a string representation of this filter.
+     */
+    @Override
+    public final String toString() {
+        final DefaultTreeTable table = new DefaultTreeTable(TableColumn.VALUE);
+        toTree(table.getRoot(), new IdentityHashMap<>());
+        return table.toString();
+    }
+
+    /**
+     * Returns a hash code value computed from the class and the children.
+     */
+    @Override
+    public int hashCode() {
+        return getClass().hashCode() + 37 * getChildren().hashCode();
+    }
+
+    /**
+     * Returns {@code true} if the given object is an instance of the same class with the equal children.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (other != null && other.getClass() == getClass()) {
+            return getChildren().equals(((Node) other).getChildren());
+        }
+        return false;
+    }
+
+    /**
+     * Reports that an operation failed because of the given exception.
+     *
+     * @todo Consider defining a {@code Context} class providing, among other information, listeners where to report warnings.
+     */
+    final void warning(final String caller, final Exception e) {
+        Logging.recoverableException(Logging.getLogger(Loggers.FILTER), getClass(), caller, e);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/filter/UnaryFunction.java b/core/sis-feature/src/main/java/org/apache/sis/filter/UnaryFunction.java
new file mode 100644
index 0000000..84bb74f
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/UnaryFunction.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.sis.util.ArgumentChecks;
+import org.opengis.filter.Filter;
+
+// Branch-dependent imports
+import org.opengis.filter.FilterVisitor;
+import org.opengis.filter.expression.Expression;
+
+
+/**
+ * Base class for filters performing operations on one value.
+ * The nature of the operation is dependent on the subclass.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+abstract class UnaryFunction extends Node implements Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 4441264252138631694L;
+
+    /**
+     * The expression to be used by this operator.
+     *
+     * @see #getExpression()
+     */
+    protected final Expression expression;
+
+    /**
+     * Creates a new unary operator.
+     */
+    UnaryFunction(final Expression expression) {
+        ArgumentChecks.ensureNonNull("expression", expression);
+        this.expression = expression;
+    }
+
+    /**
+     * Returns the expressions to be used by this operator.
+     */
+    public final Expression getExpression() {
+        return expression;
+    }
+
+    /**
+     * Returns the singleton expression tested by this operator.
+     */
+    @Override
+    protected final Collection<?> getChildren() {
+        return Collections.singleton(expression);
+    }
+
+    /**
+     * Returns a hash code value for this operator.
+     */
+    @Override
+    public final int hashCode() {
+        // We use the symbol as a way to differentiate the subclasses.
+        return expression.hashCode() ^ symbol();
+    }
+
+    /**
+     * Compares this operator with the given object for equality.
+     */
+    @Override
+    public final boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj != null && obj.getClass() == getClass()) {
+            return expression.equals(((UnaryFunction) obj).expression);
+        }
+        return false;
+    }
+
+
+    /**
+     * Filter operator that checks if an expression's value is {@code null}.  A {@code null}
+     * is equivalent to no value present. The value 0 is a valid value and is not considered
+     * {@code null}.
+     */
+    static final class IsNull extends UnaryFunction implements org.opengis.filter.PropertyIsNull {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = 5538743632585679484L;
+
+        /** Creates a new operator. */
+        IsNull(Expression expression) {
+            super(expression);
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return NAME;}
+        @Override protected char symbol() {return '∅';}
+
+        /** Returns {@code true} if the given value evaluates to {@code null}. */
+        @Override public boolean evaluate(final Object object) {
+            return expression.evaluate(object) == null;
+        }
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(FilterVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+
+
+    /**
+     * The negation filter (¬).
+     */
+    static final class Not extends Node implements org.opengis.filter.Not {
+        /** For cross-version compatibility. */
+        private static final long serialVersionUID = -1296823195138427781L;
+
+        /** The filter to negate. */
+        private final Filter filter;
+
+        /** Creates a new filter. */
+        Not(final Filter filter) {
+            ArgumentChecks.ensureNonNull("filter", filter);
+            this.filter = filter;
+        }
+
+        /** Identification of this operation. */
+        @Override protected String name() {return "Not";}
+        @Override protected char symbol() {return '¬';}
+
+        /** Returns the singleton filter used by this operation. */
+        @Override protected Collection<Filter> getChildren() {
+            return Collections.singletonList(filter);
+        }
+
+        /** Returns */
+        @Override public Filter getFilter() {
+            return filter;
+        }
+
+        /** Evaluate this filter on the given object. */
+        @Override public boolean evaluate(final Object object) {
+            return !filter.evaluate(object);
+        }
+
+        /** Implementation of the visitor pattern. */
+        @Override public Object accept(FilterVisitor visitor, Object extraData) {
+            return visitor.visit(this, extraData);
+        }
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
index e5da3ff..1be7829 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/FeatureExpression.java
@@ -38,7 +38,7 @@ public interface FeatureExpression {
      * {@link org.opengis.feature.AttributeType} or a {@link org.opengis.feature.FeatureAssociationRole}
      * but not an {@link org.opengis.feature.Operation}.
      *
-     * @param  type the type of features on which to apply this expression.
+     * @param  type  the type of features on which to apply this expression.
      * @return expected expression result type.
      * @throws IllegalArgumentException if this method can not determine the property type for the given feature type.
      */
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/ArithmeticFunctionTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/ArithmeticFunctionTest.java
new file mode 100644
index 0000000..2171c9b
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/ArithmeticFunctionTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+import org.opengis.filter.FilterFactory2;
+import org.opengis.filter.expression.Expression;
+
+import static org.apache.sis.test.Assert.*;
+
+
+/**
+ * Tests {@link ArithmeticFunction} implementations.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class ArithmeticFunctionTest extends TestCase {
+    /**
+     * The factory to use for creating the objects to test.
+     */
+    private final FilterFactory2 factory = new DefaultFilterFactory();
+
+    /**
+     * Tests "Add" (construction, evaluation, serialization, equality).
+     */
+    @Test
+    public void testAdd() {
+        Expression op = factory.add(factory.literal(10.0), factory.literal(20.0));
+        assertEquals(30.0, op.evaluate(null));
+        assertSerializedEquals(op);
+    }
+
+    /**
+     * Tests "Subtract" (construction, evaluation, serialization, equality).
+     */
+    @Test
+    public void testSubtract() {
+        Expression op = factory.subtract(factory.literal(10.0), factory.literal(20.0));
+        assertEquals(-10.0, op.evaluate(null));
+        assertSerializedEquals(op);
+    }
+
+    /**
+     * Tests "Multiply" (construction, evaluation, serialization, equality).
+     */
+    @Test
+    public void testMultiply() {
+        Expression op = factory.multiply(factory.literal(10.0), factory.literal(20.0));
+        assertEquals(200.0, op.evaluate(null));
+        assertSerializedEquals(op);
+    }
+
+    /**
+     * Tests "Divide" (construction, evaluation, serialization, equality).
+     */
+    @Test
+    public void testDivide() {
+        Expression op = factory.divide(factory.literal(10.0), factory.literal(20.0));
+        assertEquals(0.5, op.evaluate(null));
+        assertSerializedEquals(op);
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultAddTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultAddTest.java
deleted file mode 100644
index 3a762f6..0000000
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultAddTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import static org.apache.sis.test.Assert.assertSerializedEquals;
-import org.apache.sis.test.TestCase;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import org.junit.Test;
-import org.opengis.filter.FilterFactory2;
-
-/**
- * Tests {@link DefaultAdd}.
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-public class DefaultAddTest extends TestCase {
-    /**
-     * Test factory.
-     */
-    @Test
-    public void testConstructor() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        assertNotNull(factory.add(factory.literal(1), factory.literal(2)));
-    }
-
-    /**
-     * Tests evaluation.
-     */
-    @Test
-    public void testEvaluate() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-
-        assertEquals(30.0, new DefaultAdd(factory.literal(10), factory.literal(20)).evaluate(null));
-    }
-
-    /**
-     * Tests serialization.
-     */
-    @Test
-    public void testSerialize() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        assertSerializedEquals(new DefaultAdd(factory.literal(1), factory.literal(2)));
-    }
-
-}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultAndTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultAndTest.java
deleted file mode 100644
index 4db8227..0000000
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultAndTest.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import static org.apache.sis.test.Assert.assertSerializedEquals;
-import org.apache.sis.test.TestCase;
-import org.junit.Assert;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import org.junit.Test;
-import org.opengis.filter.Filter;
-import org.opengis.filter.FilterFactory2;
-import org.opengis.filter.expression.Literal;
-import org.opengis.filter.expression.PropertyName;
-
-/**
- * Tests {@link DefaultAnd}.
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-public class DefaultAndTest extends TestCase {
-    /**
-     * Test factory.
-     */
-    @Test
-    public void testConstructor() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        final Literal literal = factory.literal("text");
-        final Filter filter = factory.isNull(literal);
-
-        assertNotNull(factory.and(filter, filter));
-        assertNotNull(factory.and(Arrays.asList(filter, filter, filter)));
-
-        try {
-            factory.and(null, null);
-            Assert.fail("Creation of an AND with a null child filter must raise an exception");
-        } catch (Exception ex) {}
-        try {
-            factory.and(filter, null);
-            Assert.fail("Creation of an AND with a null child filter must raise an exception");
-        } catch (Exception ex) {}
-        try {
-            factory.and(null, filter);
-            Assert.fail("Creation of an AND with a null child filter must raise an exception");
-        } catch (Exception ex) {}
-        try {
-            factory.and(Arrays.asList(filter));
-            Assert.fail("Creation of an AND with less then two children filters must raise an exception");
-        } catch (Exception ex) {}
-
-    }
-
-    /**
-     * Tests evaluation.
-     */
-    @Test
-    public void testEvaluate() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        final PropertyName literalNotNull = factory.property("attNotNull");
-        final PropertyName literalNull = factory.property("attNull");
-        final Filter filterTrue = factory.isNull(literalNull);
-        final Filter filterFalse = factory.isNull(literalNotNull);
-
-        final Map<String,String> feature = new HashMap<>();
-        feature.put("attNotNull", "text");
-
-        assertEquals(true, new DefaultAnd(Arrays.asList(filterTrue, filterTrue)).evaluate(feature));
-        assertEquals(false, new DefaultAnd(Arrays.asList(filterFalse, filterTrue)).evaluate(feature));
-        assertEquals(false, new DefaultAnd(Arrays.asList(filterTrue, filterFalse)).evaluate(feature));
-        assertEquals(false, new DefaultAnd(Arrays.asList(filterFalse, filterFalse)).evaluate(feature));
-    }
-
-    /**
-     * Tests serialization.
-     */
-    @Test
-    public void testSerialize() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        final Literal literal = factory.literal("text");
-        final Filter filter = factory.isNull(literal);
-        assertSerializedEquals(new DefaultAnd(Arrays.asList(filter,filter)));
-    }
-
-}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultDivideTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultDivideTest.java
deleted file mode 100644
index a7a05de..0000000
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultDivideTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import static org.apache.sis.test.Assert.assertSerializedEquals;
-import org.apache.sis.test.TestCase;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import org.junit.Test;
-import org.opengis.filter.FilterFactory2;
-
-/**
- * Tests {@link DefaultDivide}.
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-public class DefaultDivideTest extends TestCase {
-    /**
-     * Test factory.
-     */
-    @Test
-    public void testConstructor() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        assertNotNull(factory.divide(factory.literal(1), factory.literal(2)));
-    }
-
-    /**
-     * Tests evaluation.
-     */
-    @Test
-    public void testEvaluate() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-
-        assertEquals(0.5, new DefaultDivide(factory.literal(10), factory.literal(20)).evaluate(null));
-    }
-
-    /**
-     * Tests serialization.
-     */
-    @Test
-    public void testSerialize() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        assertSerializedEquals(new DefaultDivide(factory.literal(1), factory.literal(2)));
-    }
-
-}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultLiteralTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultLiteralTest.java
deleted file mode 100644
index 2abab7a..0000000
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultLiteralTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Date;
-import org.apache.sis.test.TestCase;
-import org.junit.Test;
-import org.opengis.filter.FilterFactory2;
-import org.opengis.filter.expression.Literal;
-
-import static org.apache.sis.test.Assert.*;
-
-
-/**
- * Tests {@link DefaultLiteral}.
- *
- * @author Johann Sorel (Geomatys)
- * @version 0.7
- * @since   0.7
- * @module
- */
-public class DefaultLiteralTest extends TestCase {
-    /**
-     * Test factory.
-     */
-    @Test
-    public void testConstructor() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        assertNotNull(factory.literal(true));
-        assertNotNull(factory.literal("a text string"));
-        assertNotNull(factory.literal('x'));
-        assertNotNull(factory.literal(122));
-        assertNotNull(factory.literal(45.56d));
-    }
-
-    /**
-     * Tests value and evaluation.
-     */
-    @Test
-    public void testEvaluate() {
-        final Literal literal = new DefaultLiteral<>(12.45);
-        assertEquals(12.45, (Double)literal.getValue(), STRICT);
-        assertEquals(12.45, (Double)literal.evaluate(null), STRICT);
-        assertEquals(12.45, literal.evaluate(null, Double.class), STRICT);
-        assertEquals("12.45", literal.evaluate(null, String.class));
-        assertEquals(null, literal.evaluate(null, Date.class));
-    }
-
-    /**
-     * Tests serialization.
-     */
-    @Test
-    public void testSerialize() {
-        assertSerializedEquals(new DefaultLiteral<>(true));
-        assertSerializedEquals(new DefaultLiteral<>("a text string"));
-        assertSerializedEquals(new DefaultLiteral<>('x'));
-        assertSerializedEquals(new DefaultLiteral<>(122));
-        assertSerializedEquals(new DefaultLiteral<>(45.56d));
-    }
-}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultMultiplyTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultMultiplyTest.java
deleted file mode 100644
index 7ed8593..0000000
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultMultiplyTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import static org.apache.sis.test.Assert.assertSerializedEquals;
-import org.apache.sis.test.TestCase;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import org.junit.Test;
-import org.opengis.filter.FilterFactory2;
-
-/**
- * Tests {@link DefaultMultiply}.
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-public class DefaultMultiplyTest extends TestCase {
-    /**
-     * Test factory.
-     */
-    @Test
-    public void testConstructor() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        assertNotNull(factory.multiply(factory.literal(1), factory.literal(2)));
-    }
-
-    /**
-     * Tests evaluation.
-     */
-    @Test
-    public void testEvaluate() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-
-        assertEquals(200.0, new DefaultMultiply(factory.literal(10), factory.literal(20)).evaluate(null));
-    }
-
-    /**
-     * Tests serialization.
-     */
-    @Test
-    public void testSerialize() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        assertSerializedEquals(new DefaultMultiply(factory.literal(1), factory.literal(2)));
-    }
-
-}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultNotTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultNotTest.java
deleted file mode 100644
index 06bb5d5..0000000
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultNotTest.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.HashMap;
-import java.util.Map;
-import static org.apache.sis.test.Assert.assertSerializedEquals;
-import org.apache.sis.test.TestCase;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import org.junit.Test;
-import org.opengis.filter.Filter;
-import org.opengis.filter.FilterFactory2;
-import org.opengis.filter.expression.Literal;
-import org.opengis.filter.expression.PropertyName;
-
-/**
- * Tests {@link DefaultNot}.
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-public class DefaultNotTest extends TestCase {
-    /**
-     * Test factory.
-     */
-    @Test
-    public void testConstructor() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        final Literal literal = factory.literal("text");
-        final Filter filter = factory.isNull(literal);
-        assertNotNull(factory.not(filter));
-    }
-
-    /**
-     * Tests evaluation.
-     */
-    @Test
-    public void testEvaluate() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        final PropertyName literalNotNull = factory.property("attNotNull");
-        final PropertyName literalNull = factory.property("attNull");
-        final Filter filterTrue = factory.isNull(literalNull);
-        final Filter filterFalse = factory.isNull(literalNotNull);
-        final Map<String,String> feature = new HashMap<>();
-        feature.put("attNotNull", "text");
-
-        assertEquals(false, new DefaultNot(filterTrue).evaluate(feature));
-        assertEquals(true, new DefaultNot(filterFalse).evaluate(feature));
-    }
-
-    /**
-     * Tests serialization.
-     */
-    @Test
-    public void testSerialize() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        final Literal literal = factory.literal("text");
-        final Filter filter = factory.isNull(literal);
-        assertSerializedEquals(new DefaultNot(filter));
-    }
-
-}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultFeatureIdTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultObjectIdTest.java
similarity index 62%
rename from core/sis-feature/src/test/java/org/apache/sis/filter/DefaultFeatureIdTest.java
rename to core/sis-feature/src/test/java/org/apache/sis/filter/DefaultObjectIdTest.java
index c10cb70..ea2317e 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultFeatureIdTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultObjectIdTest.java
@@ -18,24 +18,23 @@ package org.apache.sis.filter;
 
 import org.apache.sis.feature.builder.AttributeRole;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
-import static org.apache.sis.test.Assert.assertSerializedEquals;
 import org.apache.sis.test.TestCase;
-import org.junit.Assert;
-import static org.junit.Assert.assertNotNull;
 import org.junit.Test;
 import org.opengis.feature.Feature;
-import org.opengis.feature.FeatureType;
 import org.opengis.filter.FilterFactory2;
 
+import static org.apache.sis.test.Assert.*;
+
+
 /**
- * Tests {@link DefaultFeatureId}.
+ * Tests {@link DefaultObjectId}.
  *
- * @author Johann Sorel (Geomatys)
+ * @author  Johann Sorel (Geomatys)
  * @version 1.0
  * @since   1.0
  * @module
  */
-public class DefaultFeatureIdTest extends TestCase {
+public final strictfp class DefaultObjectIdTest extends TestCase {
     /**
      * Test factory.
      */
@@ -47,40 +46,48 @@ public class DefaultFeatureIdTest extends TestCase {
     }
 
     /**
-     * Tests evaluation.
+     * Creates 3 features for testing purpose. Features are (in that order):
+     *
+     * <ol>
+     *   <li>A feature type with an identifier as a string.</li>
+     *   <li>A feature type with an integer identifier.</li>
+     *   <li>A feature type with no identifier.</li>
+     * </ol>
      */
-    @Test
-    public void testEvaluate() {
-        final DefaultFeatureId fid = new DefaultFeatureId("123");
-
-        // a feature type with a string identifier
+    private static Feature[] features() {
+        final Feature[] features = new Feature[3];
         final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
         ftb.setName("type1");
         ftb.addAttribute(String.class).setName("att").addRole(AttributeRole.IDENTIFIER_COMPONENT);
-        final FeatureType typeString = ftb.build();
+        Feature f = ftb.build().newInstance();
+        f.setPropertyValue("att", "123");
+        features[0] = f;
 
-        // a feature type with an integer identifier
         ftb.clear();
         ftb.setName("type2");
         ftb.addAttribute(Integer.class).setName("att").addRole(AttributeRole.IDENTIFIER_COMPONENT);
-        final FeatureType typeInt = ftb.build();
+        f = ftb.build().newInstance();
+        f.setPropertyValue("att", 123);
+        features[1] = f;
 
-        // a feature type with no identifier
         ftb.clear();
         ftb.setName("type3");
-        final FeatureType typeNone = ftb.build();
-
-        final Feature feature1 = typeString.newInstance();
-        feature1.setPropertyValue("att", "123");
-
-        final Feature feature2 = typeInt.newInstance();
-        feature2.setPropertyValue("att", 123);
+        f = ftb.build().newInstance();
+        features[2] = f;
 
-        final Feature feature3 = typeNone.newInstance();
+        return features;
+    }
 
-        Assert.assertTrue(fid.matches(feature1));
-        Assert.assertTrue(fid.matches(feature2));
-        Assert.assertFalse(fid.matches(feature3));
+    /**
+     * Tests evaluation.
+     */
+    @Test
+    public void testEvaluate() {
+        final DefaultObjectId fid = new DefaultObjectId("123");
+        final Feature[] features = features();
+        assertTrue (fid.matches(features[0]));
+        assertTrue (fid.matches(features[1]));
+        assertFalse(fid.matches(features[2]));
     }
 
     /**
@@ -88,7 +95,6 @@ public class DefaultFeatureIdTest extends TestCase {
      */
     @Test
     public void testSerialize() {
-        assertSerializedEquals(new DefaultFeatureId("abc"));
+        assertSerializedEquals(new DefaultObjectId("abc"));
     }
-
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultOrTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultOrTest.java
deleted file mode 100644
index 09537ef..0000000
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultOrTest.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.Map;
-import static org.apache.sis.test.Assert.assertSerializedEquals;
-import org.apache.sis.test.TestCase;
-import org.junit.Assert;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import org.junit.Test;
-import org.opengis.filter.Filter;
-import org.opengis.filter.FilterFactory2;
-import org.opengis.filter.expression.Literal;
-import org.opengis.filter.expression.PropertyName;
-
-/**
- * Tests {@link DefaultOr}.
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-public class DefaultOrTest extends TestCase {
-    /**
-     * Test factory.
-     */
-    @Test
-    public void testConstructor() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        final Literal literal = factory.literal("text");
-        final Filter filter = factory.isNull(literal);
-
-        assertNotNull(factory.or(filter, filter));
-        assertNotNull(factory.or(Arrays.asList(filter, filter, filter)));
-
-        try {
-            factory.or(null, null);
-            Assert.fail("Creation of an OR with a null child filter must raise an exception");
-        } catch (Exception ex) {}
-        try {
-            factory.or(filter, null);
-            Assert.fail("Creation of an OR with a null child filter must raise an exception");
-        } catch (Exception ex) {}
-        try {
-            factory.or(null, filter);
-            Assert.fail("Creation of an OR with a null child filter must raise an exception");
-        } catch (Exception ex) {}
-        try {
-            factory.or(Arrays.asList(filter));
-            Assert.fail("Creation of an OR with less then two children filters must raise an exception");
-        } catch (Exception ex) {}
-
-    }
-
-    /**
-     * Tests evaluation.
-     */
-    @Test
-    public void testEvaluate() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        final PropertyName literalNotNull = factory.property("attNotNull");
-        final PropertyName literalNull = factory.property("attNull");
-        final Filter filterTrue = factory.isNull(literalNull);
-        final Filter filterFalse = factory.isNull(literalNotNull);
-
-        final Map<String,String> feature = new HashMap<>();
-        feature.put("attNotNull", "text");
-
-        assertEquals(true, new DefaultOr(Arrays.asList(filterTrue, filterTrue)).evaluate(feature));
-        assertEquals(true, new DefaultOr(Arrays.asList(filterFalse, filterTrue)).evaluate(feature));
-        assertEquals(true, new DefaultOr(Arrays.asList(filterTrue, filterFalse)).evaluate(feature));
-        assertEquals(false, new DefaultOr(Arrays.asList(filterFalse, filterFalse)).evaluate(feature));
-    }
-
-    /**
-     * Tests serialization.
-     */
-    @Test
-    public void testSerialize() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        final Literal literal = factory.literal("text");
-        final Filter filter = factory.isNull(literal);
-        assertSerializedEquals(new DefaultOr(Arrays.asList(filter,filter)));
-    }
-
-}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultPropertyNameTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultPropertyNameTest.java
deleted file mode 100644
index 6190e1d..0000000
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultPropertyNameTest.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import java.util.Map;
-import java.util.HashMap;
-import org.opengis.filter.FilterFactory2;
-import org.opengis.filter.expression.PropertyName;
-import org.apache.sis.util.iso.Names;
-import org.apache.sis.test.TestCase;
-import org.junit.Test;
-
-import static org.apache.sis.test.Assert.*;
-
-
-/**
- * Tests {@link DefaultPropertyName}.
- *
- * @author  Johann Sorel (Geomatys)
- * @version 0.8
- * @since   0.8
- * @module
- */
-public final strictfp class DefaultPropertyNameTest extends TestCase {
-    /**
-     * Test factory.
-     */
-    @Test
-    public void testConstructor() {
-        final FilterFactory2 FF = new DefaultFilterFactory();
-        assertNotNull(FF.property(Names.parseGenericName(null, null, "type")));
-        assertNotNull(FF.property("type"));
-    }
-
-    /**
-     * Tests evaluation.
-     */
-    @Test
-    public void testEvaluate() {
-        final Map<String,String> candidate = new HashMap<>();
-
-        final PropertyName prop = new DefaultPropertyName("type");
-        assertEquals("type", prop.getPropertyName());
-
-        assertEquals(null, prop.evaluate(candidate));
-        assertEquals(null, prop.evaluate(null));
-
-        candidate.put("type", "road");
-        assertEquals("road", prop.evaluate(candidate));
-        assertEquals("road", prop.evaluate(candidate,String.class));
-
-        candidate.put("type", "45.1");
-        assertEquals("45.1", prop.evaluate(candidate));
-        assertEquals("45.1", prop.evaluate(candidate, Object.class));
-        assertEquals("45.1", prop.evaluate(candidate, String.class));
-        assertEquals( 45.1,  prop.evaluate(candidate, Double.class), STRICT);
-    }
-
-    /**
-     * Tests serialization.
-     */
-    @Test
-    public void testSerialize() {
-        assertSerializedEquals(new DefaultPropertyName("type"));
-    }
-}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultSubtractTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultSubtractTest.java
deleted file mode 100644
index 3e85dd8..0000000
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultSubtractTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.filter;
-
-import static org.apache.sis.test.Assert.assertSerializedEquals;
-import org.apache.sis.test.TestCase;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import org.junit.Test;
-import org.opengis.filter.FilterFactory2;
-
-/**
- * Tests {@link DefaultSubtract}.
- *
- * @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since   1.0
- * @module
- */
-public class DefaultSubtractTest extends TestCase {
-    /**
-     * Test factory.
-     */
-    @Test
-    public void testConstructor() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        assertNotNull(factory.subtract(factory.literal(1), factory.literal(2)));
-    }
-
-    /**
-     * Tests evaluation.
-     */
-    @Test
-    public void testEvaluate() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-
-        assertEquals(-10.0, new DefaultSubtract(factory.literal(10), factory.literal(20)).evaluate(null));
-    }
-
-    /**
-     * Tests serialization.
-     */
-    @Test
-    public void testSerialize() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        assertSerializedEquals(new DefaultSubtract(factory.literal(1), factory.literal(2)));
-    }
-
-}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultIdTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/FilterByIdentifierTest.java
similarity index 75%
rename from core/sis-feature/src/test/java/org/apache/sis/filter/DefaultIdTest.java
rename to core/sis-feature/src/test/java/org/apache/sis/filter/FilterByIdentifierTest.java
index 87d6dc6..ee0a8c4 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/DefaultIdTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/FilterByIdentifierTest.java
@@ -21,31 +21,35 @@ import java.util.HashSet;
 import java.util.Set;
 import org.apache.sis.feature.builder.AttributeRole;
 import org.apache.sis.feature.builder.FeatureTypeBuilder;
-import static org.apache.sis.test.Assert.assertSerializedEquals;
 import org.apache.sis.test.TestCase;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
 import org.junit.Test;
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
 import org.opengis.filter.FilterFactory2;
 import org.opengis.filter.identity.Identifier;
 
+import static org.apache.sis.test.Assert.*;
+
+
 /**
- * Tests {@link DefaultId}.
+ * Tests {@link FilterByIdentifier}.
  *
- * @author Johann Sorel (Geomatys)
+ * @author  Johann Sorel (Geomatys)
  * @version 1.0
  * @since   1.0
  * @module
  */
-public class DefaultIdTest extends TestCase {
+public final strictfp class FilterByIdentifierTest extends TestCase {
+    /**
+     * The factory to use for creating the objects to test.
+     */
+    private final FilterFactory2 factory = new DefaultFilterFactory();
+
     /**
      * Test factory.
      */
     @Test
     public void testConstructor() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
         assertNotNull(factory.id(Collections.singleton(factory.featureId("abc"))));
     }
 
@@ -54,14 +58,11 @@ public class DefaultIdTest extends TestCase {
      */
     @Test
     public void testEvaluate() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-
         final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
         ftb.setName("type");
         ftb.addAttribute(String.class).setName("att").addRole(AttributeRole.IDENTIFIER_COMPONENT);
         final FeatureType type = ftb.build();
 
-
         final Feature feature1 = type.newInstance();
         feature1.setPropertyValue("att", "123");
 
@@ -74,11 +75,11 @@ public class DefaultIdTest extends TestCase {
         final Set<Identifier> ids = new HashSet<>();
         ids.add(factory.featureId("abc"));
         ids.add(factory.featureId("123"));
-        final DefaultId id = new DefaultId(ids);
+        final FilterByIdentifier id = new FilterByIdentifier(ids);
 
-        assertEquals(true, id.evaluate(feature1));
-        assertEquals(true, id.evaluate(feature2));
-        assertEquals(false, id.evaluate(feature3));
+        assertTrue (id.evaluate(feature1));
+        assertTrue (id.evaluate(feature2));
+        assertFalse(id.evaluate(feature3));
     }
 
     /**
@@ -86,8 +87,6 @@ public class DefaultIdTest extends TestCase {
      */
     @Test
     public void testSerialize() {
-        final FilterFactory2 factory = new DefaultFilterFactory();
-        assertSerializedEquals(new DefaultId(Collections.singleton(factory.featureId("abc"))));
+        assertSerializedEquals(new FilterByIdentifier(Collections.singleton(factory.featureId("abc"))));
     }
-
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/LeafExpressionTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/LeafExpressionTest.java
new file mode 100644
index 0000000..f6d4418
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/LeafExpressionTest.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.Date;
+import java.util.Map;
+import java.util.HashMap;
+import org.opengis.filter.FilterFactory2;
+import org.opengis.filter.expression.Literal;
+import org.opengis.filter.expression.PropertyName;
+import org.apache.sis.util.iso.Names;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.apache.sis.test.Assert.*;
+
+
+/**
+ * Tests {@link LeafExpression}.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class LeafExpressionTest extends TestCase {
+    /**
+     * The factory to use for creating the objects to test.
+     */
+    private final FilterFactory2 factory = new DefaultFilterFactory();
+
+    /**
+     * Test creation of "PropertyName".
+     */
+    @Test
+    public void testPropertyConstructor() {
+        assertNotNull(factory.property(Names.parseGenericName(null, null, "type")));
+        assertNotNull(factory.property("type"));
+    }
+
+    /**
+     * Test creation of "Literal".
+     */
+    @Test
+    public void testLiteralConstructor() {
+        assertNotNull(factory.literal(true));
+        assertNotNull(factory.literal("a text string"));
+        assertNotNull(factory.literal('x'));
+        assertNotNull(factory.literal(122));
+        assertNotNull(factory.literal(45.56d));
+    }
+
+    /**
+     * Tests evaluation of "PropertyName".
+     */
+    @Test
+    public void testPropertyEvaluate() {
+        final Map<String,String> candidate = new HashMap<>();
+
+        final PropertyName prop = factory.property("type");
+        assertEquals("type", prop.getPropertyName());
+
+        assertNull(prop.evaluate(candidate));
+        assertNull(prop.evaluate(null));
+
+        candidate.put("type", "road");
+        assertEquals("road", prop.evaluate(candidate));
+        assertEquals("road", prop.evaluate(candidate, String.class));
+
+        candidate.put("type", "45.1");
+        assertEquals("45.1", prop.evaluate(candidate));
+        assertEquals("45.1", prop.evaluate(candidate, Object.class));
+        assertEquals("45.1", prop.evaluate(candidate, String.class));
+        assertEquals( 45.1,  prop.evaluate(candidate, Double.class), STRICT);
+    }
+
+    /**
+     * Tests evaluation of "Literal".
+     */
+    @Test
+    public void testLiteralEvaluate() {
+        final Literal literal = factory.literal(12.45);
+        assertEquals(12.45,   literal.getValue());
+        assertEquals(12.45,   literal.evaluate(null));
+        assertEquals(12.45,   literal.evaluate(null, Double.class), STRICT);
+        assertEquals("12.45", literal.evaluate(null, String.class));
+        assertNull  (         literal.evaluate(null, Date.class));
+    }
+
+    /**
+     * Tests serialization of "PropertyName".
+     */
+    @Test
+    public void testPropertySerialize() {
+        assertSerializedEquals(factory.property("type"));
+    }
+
+    /**
+     * Tests serialization of "Literal".
+     */
+    @Test
+    public void testLiteralSerialize() {
+        assertSerializedEquals(factory.literal(true));
+        assertSerializedEquals(factory.literal("a text string"));
+        assertSerializedEquals(factory.literal('x'));
+        assertSerializedEquals(factory.literal(122));
+        assertSerializedEquals(factory.literal(45.56d));
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFunctionTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFunctionTest.java
new file mode 100644
index 0000000..f9cdb1b
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFunctionTest.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.Map;
+import java.util.Arrays;
+import java.util.Collections;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+import org.opengis.filter.Filter;
+import org.opengis.filter.FilterFactory2;
+import org.opengis.filter.expression.Literal;
+
+import static org.apache.sis.test.Assert.*;
+
+
+/**
+ * Tests {@link LogicalFunction} implementations.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class LogicalFunctionTest extends TestCase {
+    /**
+     * The factory to use for creating the objects to test.
+     */
+    private final FilterFactory2 factory = new DefaultFilterFactory();
+
+    /**
+     * Tests creation of "And" expression from the factory.
+     */
+    @Test
+    public void testAndConstructor() {
+        final Filter filter = factory.isNull(factory.literal("text"));
+        assertNotNull(factory.and(filter, filter));
+        assertNotNull(factory.and(Arrays.asList(filter, filter, filter)));
+        try {
+            factory.and(null, null);
+            fail("Creation of an AND with a null child filter must raise an exception");
+        } catch (NullPointerException ex) {}
+        try {
+            factory.and(filter, null);
+            fail("Creation of an AND with a null child filter must raise an exception");
+        } catch (NullPointerException ex) {}
+        try {
+            factory.and(null, filter);
+            fail("Creation of an AND with a null child filter must raise an exception");
+        } catch (NullPointerException ex) {}
+        try {
+            factory.and(Arrays.asList(filter));
+            fail("Creation of an AND with less then two children filters must raise an exception");
+        } catch (IllegalArgumentException ex) {}
+    }
+
+    /**
+     * Tests creation of "Or" expression from the factory.
+     */
+    @Test
+    public void testOrConstructor() {
+        final Filter filter = factory.isNull(factory.literal("text"));
+        assertNotNull(factory.or(filter, filter));
+        assertNotNull(factory.or(Arrays.asList(filter, filter, filter)));
+        try {
+            factory.or(null, null);
+            fail("Creation of an OR with a null child filter must raise an exception");
+        } catch (NullPointerException ex) {}
+        try {
+            factory.or(filter, null);
+            fail("Creation of an OR with a null child filter must raise an exception");
+        } catch (NullPointerException ex) {}
+        try {
+            factory.or(null, filter);
+            fail("Creation of an OR with a null child filter must raise an exception");
+        } catch (NullPointerException ex) {}
+        try {
+            factory.or(Arrays.asList(filter));
+            fail("Creation of an OR with less then two children filters must raise an exception");
+        } catch (IllegalArgumentException ex) {}
+    }
+
+    /**
+     * Tests evaluation of "And" expression.
+     */
+    @Test
+    public void testAndEvaluate() {
+        final Filter filterTrue  = factory.isNull(factory.property("attNull"));
+        final Filter filterFalse = factory.isNull(factory.property("attNotNull"));
+        final Map<String,String> feature = Collections.singletonMap("attNotNull", "text");
+
+        assertTrue (factory.and(filterTrue,  filterTrue ).evaluate(feature));
+        assertFalse(factory.and(filterFalse, filterTrue ).evaluate(feature));
+        assertFalse(factory.and(filterTrue,  filterFalse).evaluate(feature));
+        assertFalse(factory.and(filterFalse, filterFalse).evaluate(feature));
+    }
+
+    /**
+     * Tests evaluation of "Or" expression.
+     */
+    @Test
+    public void testOrEvaluate() {
+        final Filter filterTrue  = factory.isNull(factory.property("attNull"));
+        final Filter filterFalse = factory.isNull(factory.property("attNotNull"));
+        final Map<String,String> feature = Collections.singletonMap("attNotNull", "text");
+
+        assertTrue (factory.or(filterTrue,  filterTrue ).evaluate(feature));
+        assertTrue (factory.or(filterFalse, filterTrue ).evaluate(feature));
+        assertTrue (factory.or(filterTrue,  filterFalse).evaluate(feature));
+        assertFalse(factory.or(filterFalse, filterFalse).evaluate(feature));
+    }
+
+    /**
+     * Tests serialization of "And" expression.
+     */
+    @Test
+    public void testAndSerialize() {
+        final Literal literal = factory.literal("text");
+        final Filter  filter  = factory.isNull(literal);
+        assertSerializedEquals(factory.and(filter, filter));
+    }
+
+    /**
+     * Tests serialization of "Or" expression.
+     */
+    @Test
+    public void testOrSerialize() {
+        final Literal literal = factory.literal("text");
+        final Filter  filter  = factory.isNull(literal);
+        assertSerializedEquals(factory.or(filter, filter));
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/UnaryFunctionTest.java b/core/sis-feature/src/test/java/org/apache/sis/filter/UnaryFunctionTest.java
new file mode 100644
index 0000000..2fc0d0a
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/UnaryFunctionTest.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.filter;
+
+import java.util.Map;
+import java.util.Collections;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+import org.opengis.filter.Filter;
+import org.opengis.filter.FilterFactory2;
+import org.opengis.filter.expression.Literal;
+
+import static org.apache.sis.test.Assert.*;
+
+
+/**
+ * Tests {@link UnaryFunction}.
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final strictfp class UnaryFunctionTest extends TestCase {
+    /**
+     * The factory to use for creating the objects to test.
+     */
+    private final FilterFactory2 factory = new DefaultFilterFactory();
+
+    /**
+     * Test factory with the "Not" expression.
+     */
+    @Test
+    public void testNotConstructor() {
+        final Literal literal = factory.literal("text");
+        final Filter  filter  = factory.isNull(literal);
+        assertNotNull(factory.not(filter));
+    }
+
+    /**
+     * Tests evaluation of "Not" expression.
+     */
+    @Test
+    public void testNotEvaluate() {
+        final Filter filterTrue  = factory.isNull(factory.property("attNull"));
+        final Filter filterFalse = factory.isNull(factory.property("attNotNull"));
+        final Map<String,String> feature = Collections.singletonMap("attNotNull", "text");
+
+        assertFalse(factory.not(filterTrue ).evaluate(feature));
+        assertTrue (factory.not(filterFalse).evaluate(feature));
+    }
+
+    /**
+     * Tests serialization of "Not" expression.
+     */
+    @Test
+    public void testNotSerialize() {
+        final Literal literal = factory.literal("text");
+        final Filter  filter  = factory.isNull(literal);
+        assertSerializedEquals(factory.not(filter));
+    }
+}
diff --git 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
index 43edeae..9682455 100644
--- 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
@@ -49,17 +49,12 @@ import org.junit.BeforeClass;
     org.apache.sis.feature.EnvelopeOperationTest.class,
     org.apache.sis.feature.FeatureFormatTest.class,
     org.apache.sis.feature.FeaturesTest.class,
-    org.apache.sis.filter.DefaultLiteralTest.class,
-    org.apache.sis.filter.DefaultPropertyNameTest.class,
-    org.apache.sis.filter.DefaultAndTest.class,
-    org.apache.sis.filter.DefaultOrTest.class,
-    org.apache.sis.filter.DefaultNotTest.class,
-    org.apache.sis.filter.DefaultFeatureIdTest.class,
-    org.apache.sis.filter.DefaultIdTest.class,
-    org.apache.sis.filter.DefaultAddTest.class,
-    org.apache.sis.filter.DefaultDivideTest.class,
-    org.apache.sis.filter.DefaultMultiplyTest.class,
-    org.apache.sis.filter.DefaultSubtractTest.class,
+    org.apache.sis.filter.LeafExpressionTest.class,
+    org.apache.sis.filter.LogicalFunctionTest.class,
+    org.apache.sis.filter.UnaryFunctionTest.class,
+    org.apache.sis.filter.DefaultObjectIdTest.class,
+    org.apache.sis.filter.FilterByIdentifierTest.class,
+    org.apache.sis.filter.ArithmeticFunctionTest.class,
     org.apache.sis.internal.feature.AttributeConventionTest.class,
     org.apache.sis.internal.feature.j2d.ShapePropertiesTest.class,
     org.apache.sis.internal.feature.Java2DTest.class,
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/converter/DateConverter.java b/core/sis-utility/src/main/java/org/apache/sis/internal/converter/DateConverter.java
index 3fc64ee..7ce856c 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/converter/DateConverter.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/converter/DateConverter.java
@@ -118,7 +118,7 @@ abstract class DateConverter<T> extends SystemConverter<Date,T> {
     public static final class Long extends DateConverter<java.lang.Long> {
         private static final long serialVersionUID = 5145114630594761657L;
 
-        public Long() { // Instantiated by ServiceLoader.
+        public Long() {                     // Instantiated by ServiceLoader.
             super(java.lang.Long.class);
             inverse = new Inverse(this);
         }
@@ -139,7 +139,7 @@ abstract class DateConverter<T> extends SystemConverter<Date,T> {
     public static final class SQL extends DateConverter<java.sql.Date> {
         private static final long serialVersionUID = -7444502675467008640L;
 
-        public SQL() { // Instantiated by ServiceLoader.
+        public SQL() {                      // Instantiated by ServiceLoader.
             super(java.sql.Date.class);
             inverse = new IdentityConverter<>(targetClass, Date.class, this);
         }
@@ -159,7 +159,7 @@ abstract class DateConverter<T> extends SystemConverter<Date,T> {
     public static final class Timestamp extends DateConverter<java.sql.Timestamp> {
         private static final long serialVersionUID = 7629460512978844462L;
 
-        public Timestamp() { // Instantiated by ServiceLoader.
+        public Timestamp() {                    // Instantiated by ServiceLoader.
             super(java.sql.Timestamp.class);
             inverse = new IdentityConverter<>(targetClass, Date.class, this);
         }
@@ -171,4 +171,13 @@ abstract class DateConverter<T> extends SystemConverter<Date,T> {
             return new java.sql.Timestamp(source.getTime());
         }
     }
+
+    /*
+     * We do not yet provide converter to java.time.Instant. If we do so, we need to create an InstantConverter class
+     * doing the inverse conversion.  Reminder: java.sql.Date and java.sql.Time are not convertible to Instant (their
+     * Date.toInstant() method throws UnsupportedOperationException), but java.sql.Timestamp is.
+     *
+     * If conversion to/from java.time.Instant is added, see if some code can be shared with
+     * org.apache.sis.filter.ComparisonFunction.
+     */
 }
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/system/Loggers.java b/core/sis-utility/src/main/java/org/apache/sis/internal/system/Loggers.java
index ef8f011..382f21e 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/system/Loggers.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/system/Loggers.java
@@ -35,7 +35,7 @@ import org.apache.sis.util.logging.Logging;
  * However we also have a few more specialized loggers, which are listed here.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.6
  * @module
  */
@@ -78,6 +78,11 @@ public final class Loggers extends Static {
     public static final String WKT = "org.apache.sis.io.wkt";
 
     /**
+     * The logger for operations related to filters.
+     */
+    public static final String FILTER = "org.apache.sis.filter";
+
+    /**
      * The logger for operations related to geometries.
      */
     public static final String GEOMETRY = "org.apache.sis.geometry";


Mime
View raw message