sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1831365 [1/2] - in /sis/branches/JDK8: core/sis-feature/src/main/java/org/apache/sis/filter/ storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ storage/sis-storage/src/test/java/org/apache/sis/internal/storage/
Date Thu, 10 May 2018 19:08:39 GMT
Author: desruisseaux
Date: Thu May 10 19:08:39 2018
New Revision: 1831365

URL: http://svn.apache.org/viewvc?rev=1831365&view=rev
Log:
Implement JoinFeatureSet.features(boolean) on top of Spliterator instead than Iterator,
and use an identifier created by FeatureOperations.compound(…) instead than computing
the string concatenation unconditionally for every features.

Added:
    sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryOperator.java
      - copied, changed from r1831270, sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java
    sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractUnaryOperator.java
      - copied, changed from r1831270, sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java
    sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsNull.java
      - copied, changed from r1831270, sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java
Modified:
    sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java
    sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
    sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java
    sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java
    sis/branches/JDK8/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/JoinFeatureSetTest.java

Copied: sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryOperator.java (from r1831270, sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java)
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryOperator.java?p2=sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryOperator.java&p1=sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java&r1=1831270&r2=1831365&rev=1831365&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractBinaryOperator.java [UTF-8] Thu May 10 19:08:39 2018
@@ -17,145 +17,97 @@
 package org.apache.sis.filter;
 
 import java.io.Serializable;
-import org.opengis.filter.BinaryComparisonOperator;
-import org.opengis.filter.MatchAction;
+import org.opengis.filter.Filter;
 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.
+ * 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 AbstractComparisonOperator implements BinaryComparisonOperator, Serializable {
+abstract class AbstractBinaryOperator implements Filter, Serializable {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -1401452229232869720L;
+    private static final long serialVersionUID = -5079157703540568112L;
 
     /**
-     * The first of the two expressions to be compared by this operator.
+     * 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 compared by this operator.
+     * The second of the two expressions to be used by this operator.
      *
      * @see #getExpression2()
      */
     protected final Expression expression2;
 
     /**
-     * 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.
+     * Creates a new binary 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)
-    {
+    AbstractBinaryOperator(final Expression expression1, final Expression expression2) {
         this.expression1 = expression1;
         this.expression2 = expression2;
-        this.matchCase   = matchCase;
-        this.matchAction = matchAction;
     }
 
     /**
-     * Returns the mathematical symbol for this comparison operator.
-     * The symbol should be one of the following: {@literal < > ≤ ≥ = ≠}.
+     * 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 compared by this operator.
+     * 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 compared by this operator.
+     * Returns the second of the two expressions to be used by this operator.
      */
-    @Override
     public final Expression getExpression2() {
         return expression2;
     }
 
     /**
-     * Specifies whether comparisons are case sensitive.
-     *
-     * @return {@code true} if the comparisons are case sensitive, otherwise {@code false}.
+     * Returns a hash code value for this operator.
      */
     @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 = (31 * expression1.hashCode() + expression2.hashCode()) * 37 + matchAction.hashCode();
-        if (matchCase) hash = ~hash;
-        return hash ^ symbol();             // Use the symbol as a way to differentiate the subclasses.
+    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 final boolean equals(final Object obj) {
-        if (obj == this) {
-            return true;
-        }
+    public boolean equals(final Object obj) {
         if (obj != null && obj.getClass() == getClass()) {
-            final AbstractComparisonOperator other = (AbstractComparisonOperator) obj;
-            return matchCase   ==     other.matchCase &&
-                   expression1.equals(other.expression1) &&
-                   expression2.equals(other.expression2) &&
-                   matchAction.equals(other.matchAction);
+            final AbstractBinaryOperator other = (AbstractBinaryOperator) obj;
+            return expression1.equals(other.expression1) &&
+                   expression2.equals(other.expression2);
         }
         return false;
     }
 
     /**
-     * Returns a string representation of this comparison operator.
+     * Returns a string representation of this operator.
      */
     @Override
-    public final String toString() {
+    public String toString() {
         return new StringBuilder(30).append(expression1).append(' ').append(symbol()).append(' ')
                                     .append(expression2).toString();
     }

Modified: sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java?rev=1831365&r1=1831364&r2=1831365&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java [UTF-8] Thu May 10 19:08:39 2018
@@ -27,29 +27,16 @@ import org.opengis.filter.expression.Exp
  * 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 implements BinaryComparisonOperator, Serializable {
+abstract class AbstractComparisonOperator extends AbstractBinaryOperator implements BinaryComparisonOperator, Serializable {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -1401452229232869720L;
-
-    /**
-     * The first of the two expressions to be compared by this operator.
-     *
-     * @see #getExpression1()
-     */
-    protected final Expression expression1;
-
-    /**
-     * The second of the two expressions to be compared by this operator.
-     *
-     * @see #getExpression2()
-     */
-    protected final Expression expression2;
+    private static final long serialVersionUID = -4709016194087609721L;
 
     /**
      * Whether comparisons are case sensitive.
@@ -72,35 +59,12 @@ abstract class AbstractComparisonOperato
     AbstractComparisonOperator(final Expression expression1, final Expression expression2,
                                final boolean matchCase, final MatchAction matchAction)
     {
-        this.expression1 = expression1;
-        this.expression2 = expression2;
+        super(expression1, expression2);
         this.matchCase   = matchCase;
         this.matchAction = matchAction;
     }
 
     /**
-     * Returns the mathematical symbol for this comparison operator.
-     * The symbol should be one of the following: {@literal < > ≤ ≥ = ≠}.
-     */
-    protected abstract char symbol();
-
-    /**
-     * Returns the first of the two expressions to be compared by this operator.
-     */
-    @Override
-    public final Expression getExpression1() {
-        return expression1;
-    }
-
-    /**
-     * Returns the second of the two expressions to be compared by this operator.
-     */
-    @Override
-    public final Expression getExpression2() {
-        return expression2;
-    }
-
-    /**
      * Specifies whether comparisons are case sensitive.
      *
      * @return {@code true} if the comparisons are case sensitive, otherwise {@code false}.
@@ -128,9 +92,9 @@ abstract class AbstractComparisonOperato
      */
     @Override
     public final int hashCode() {
-        int hash = (31 * expression1.hashCode() + expression2.hashCode()) * 37 + matchAction.hashCode();
+        int hash = super.hashCode() * 37 + matchAction.hashCode();
         if (matchCase) hash = ~hash;
-        return hash ^ symbol();             // Use the symbol as a way to differentiate the subclasses.
+        return hash;
     }
 
     /**
@@ -141,22 +105,10 @@ abstract class AbstractComparisonOperato
         if (obj == this) {
             return true;
         }
-        if (obj != null && obj.getClass() == getClass()) {
+        if (super.equals(obj)) {
             final AbstractComparisonOperator other = (AbstractComparisonOperator) obj;
-            return matchCase   ==     other.matchCase &&
-                   expression1.equals(other.expression1) &&
-                   expression2.equals(other.expression2) &&
-                   matchAction.equals(other.matchAction);
+            return matchCase == other.matchCase && matchAction.equals(other.matchAction);
         }
         return false;
     }
-
-    /**
-     * Returns a string representation of this comparison operator.
-     */
-    @Override
-    public final String toString() {
-        return new StringBuilder(30).append(expression1).append(' ').append(symbol()).append(' ')
-                                    .append(expression2).toString();
-    }
 }

Copied: sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractUnaryOperator.java (from r1831270, sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java)
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractUnaryOperator.java?p2=sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractUnaryOperator.java&p1=sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java&r1=1831270&r2=1831365&rev=1831365&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractComparisonOperator.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/AbstractUnaryOperator.java [UTF-8] Thu May 10 19:08:39 2018
@@ -17,146 +17,78 @@
 package org.apache.sis.filter;
 
 import java.io.Serializable;
-import org.opengis.filter.BinaryComparisonOperator;
-import org.opengis.filter.MatchAction;
+import org.opengis.filter.Filter;
 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.
+ * 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 AbstractComparisonOperator implements BinaryComparisonOperator, Serializable {
+abstract class AbstractUnaryOperator implements Filter, Serializable {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -1401452229232869720L;
+    private static final long serialVersionUID = -8183962550739028650L;
 
     /**
-     * The first of the two expressions to be compared by this operator.
+     * The expression to be used by this operator.
      *
-     * @see #getExpression1()
+     * @see #getExpression()
      */
-    protected final Expression expression1;
+    protected final Expression expression;
 
     /**
-     * The second of the two expressions to be compared by this operator.
-     *
-     * @see #getExpression2()
-     */
-    protected final Expression expression2;
-
-    /**
-     * 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.
+     * Creates a new unary 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)
-    {
-        this.expression1 = expression1;
-        this.expression2 = expression2;
-        this.matchCase   = matchCase;
-        this.matchAction = matchAction;
+    AbstractUnaryOperator(final Expression expression) {
+        this.expression = expression;
     }
 
     /**
-     * Returns the mathematical symbol for this comparison operator.
-     * The symbol should be one of the following: {@literal < > ≤ ≥ = ≠}.
+     * Returns the mathematical symbol for this operator.
      */
     protected abstract char symbol();
 
     /**
-     * Returns the first of the two expressions to be compared by this operator.
+     * Returns the expressions to be used by this operator.
      */
-    @Override
-    public final Expression getExpression1() {
-        return expression1;
+    public final Expression getExpression() {
+        return expression;
     }
 
     /**
-     * Returns the second of the two expressions to be compared by this operator.
+     * Returns a hash code value for this operator.
      */
     @Override
-    public final Expression getExpression2() {
-        return expression2;
-    }
-
-    /**
-     * 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 = (31 * expression1.hashCode() + expression2.hashCode()) * 37 + matchAction.hashCode();
-        if (matchCase) hash = ~hash;
-        return hash ^ symbol();             // Use the symbol as a way to differentiate the subclasses.
+    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 final boolean equals(final Object obj) {
-        if (obj == this) {
-            return true;
-        }
+    public boolean equals(final Object obj) {
         if (obj != null && obj.getClass() == getClass()) {
-            final AbstractComparisonOperator other = (AbstractComparisonOperator) obj;
-            return matchCase   ==     other.matchCase &&
-                   expression1.equals(other.expression1) &&
-                   expression2.equals(other.expression2) &&
-                   matchAction.equals(other.matchAction);
+            return expression.equals(((AbstractUnaryOperator) obj).expression);
         }
         return false;
     }
 
     /**
-     * Returns a string representation of this comparison operator.
+     * Returns a string representation of this operator.
      */
     @Override
-    public final String toString() {
-        return new StringBuilder(30).append(expression1).append(' ').append(symbol()).append(' ')
-                                    .append(expression2).toString();
+    public String toString() {
+        return new StringBuilder(30).append(expression).append(':').append(symbol()).toString();
     }
 }

Modified: sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java?rev=1831365&r1=1831364&r2=1831365&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java [UTF-8] Thu May 10 19:08:39 2018
@@ -523,8 +523,9 @@ public class DefaultFilterFactory implem
      * {@inheritDoc }
      */
     @Override
-    public PropertyIsNull isNull(final Expression expr) {
-        throw new UnsupportedOperationException("Not supported yet.");
+    public PropertyIsNull isNull(final Expression expression) {
+        ArgumentChecks.ensureNonNull("expression", expression);
+        return new DefaultPropertyIsNull(expression);
     }
 
     /**

Modified: sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java?rev=1831365&r1=1831364&r2=1831365&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java [UTF-8] Thu May 10 19:08:39 2018
@@ -28,6 +28,7 @@ import org.opengis.filter.expression.Exp
  * 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
@@ -36,7 +37,7 @@ final class DefaultPropertyIsEqualTo ext
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -5549267988142039640L;
+    private static final long serialVersionUID = -5783347523815670017L;
 
     /**
      * Creates a new comparison operator.

Copied: sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsNull.java (from r1831270, sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java)
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsNull.java?p2=sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsNull.java&p1=sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java&r1=1831270&r2=1831365&rev=1831365&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsEqualTo.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultPropertyIsNull.java [UTF-8] Thu May 10 19:08:39 2018
@@ -16,80 +16,58 @@
  */
 package org.apache.sis.filter;
 
-import java.util.Objects;
-import org.apache.sis.util.Numbers;
+import java.io.Serializable;
 import org.opengis.filter.FilterVisitor;
-import org.opengis.filter.MatchAction;
-import org.opengis.filter.PropertyIsEqualTo;
+import org.opengis.filter.PropertyIsNull;
 import org.opengis.filter.expression.Expression;
 
 
 /**
- * Filter operator that compares that its two sub-expressions are equal to each other.
+ * 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 DefaultPropertyIsEqualTo extends AbstractComparisonOperator implements PropertyIsEqualTo {
+final class DefaultPropertyIsNull extends AbstractUnaryOperator implements PropertyIsNull, Serializable {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -5549267988142039640L;
+    private static final long serialVersionUID = 3942075458551232678L;
 
     /**
-     * Creates a new comparison operator.
+     * Creates a new 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);
+    DefaultPropertyIsNull(final Expression expression) {
+        super(expression);
     }
 
     /**
-     * Returns the mathematical symbol for this comparison operator.
+     * Returns the null symbol, to be used in string representation.
      */
     @Override
     protected char symbol() {
-        return '=';
+        return '∅';
     }
 
     /**
-     * Determines if the test represented by this filter passed.
-     *
-     * @todo Use locale-sensitive {@link java.text.Collator} for string comparisons.
+     * Returns {@code true} if the given value evaluates to {@code null}.
      */
     @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;
+    public boolean evaluate(final Object object) {
+        return expression.evaluate(object) == null;
     }
 
     /**
      * Accepts a visitor.
      */
     @Override
-    public Object accept(FilterVisitor visitor, Object extraData) {
+    public Object accept(final FilterVisitor visitor, final Object extraData) {
         return visitor.visit(this, extraData);
     }
 }

Modified: sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java?rev=1831365&r1=1831364&r2=1831365&view=diff
==============================================================================
--- sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java [UTF-8] (original)
+++ sis/branches/JDK8/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/JoinFeatureSet.java [UTF-8] Thu May 10 19:08:39 2018
@@ -16,431 +16,548 @@
  */
 package org.apache.sis.internal.storage;
 
-import java.util.Arrays;
-import java.util.Iterator;
+import java.util.Map;
+import java.util.Collections;
 import java.util.Spliterator;
-import java.util.Spliterators;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
-import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.opengis.util.GenericName;
+import org.opengis.geometry.Envelope;
+import org.apache.sis.feature.FeatureOperations;
+import org.apache.sis.feature.DefaultFeatureType;
+import org.apache.sis.feature.DefaultAssociationRole;
 import org.apache.sis.filter.DefaultFilterFactory;
 import org.apache.sis.internal.feature.AttributeConvention;
 import org.apache.sis.internal.storage.query.SimpleQuery;
-import org.apache.sis.metadata.iso.DefaultMetadata;
-import org.apache.sis.metadata.iso.citation.DefaultCitation;
-import org.apache.sis.metadata.iso.identification.DefaultDataIdentification;
-import org.apache.sis.referencing.NamedIdentifier;
 import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.collection.BackingStoreException;
-import org.apache.sis.util.iso.SimpleInternationalString;
+import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.logging.WarningListeners;
+
+// Branch-dependent imports
 import org.opengis.feature.Feature;
 import org.opengis.feature.FeatureType;
+import org.opengis.feature.Operation;
+import org.opengis.feature.PropertyType;
+import org.opengis.filter.Filter;
 import org.opengis.filter.FilterFactory;
-import org.opengis.filter.MatchAction;
 import org.opengis.filter.PropertyIsEqualTo;
 import org.opengis.filter.expression.Expression;
-import org.opengis.geometry.Envelope;
-import org.opengis.metadata.Metadata;
-import org.opengis.util.GenericName;
+
 
 /**
- * A join feature set merges features from two different sources following a
- * SQL Join condition.
+ * Features containing association to features from two different sources, joined by a SQL-like {@code JOIN} condition.
+ * Each feature in this {@code FeatureSet} contains two or three properties:
  *
- * <p>
- * This implementation is read-only.
- * </p>
+ * <ul>
+ *   <li>An optional identifier created from the identifiers of the left and right features.</li>
+ *   <li>Zero or one association to a "left"  feature.</li>
+ *   <li>Zero or one association to a "right" feature.</li>
+ * </ul>
  *
- * @author Johann Sorel (Geomatys)
+ * The left and right features appear together in an {@code JoinFeatureSet} instance when a value from
+ * {@code leftProperty} in the first feature is equal to a value from {@code rightProperty} in the second feature.
+ *
+ * <div class="section">Implementation note</div>
+ * If iterations in one feature set is cheaper than iterations in the other feature set, then the "costly" or larger
+ * {@code FeatureSet} should be on the left side and the "cheap" {@code FeatureSet} should be on the right side.
+ *
+ * <p>This implementation is read-only.</p>
+ *
+ * @author  Johann Sorel (Geomatys)
+ * @author  Martin Desruisseaux (Geomatys)
  * @version 1.0
  * @since   1.0
  * @module
  */
-public class JoinFeatureSet extends AbstractFeatureSet implements FeatureSet {
+public class JoinFeatureSet extends AbstractFeatureSet {
     /**
-     * Join operation type.
+     * Specifies whether values on both sides are required (inner join), or only one side (outer join).
      */
     public enum Type {
         /**
-         * Both side must have a value to be included.
+         * Only records having a value on both side will be included.
+         * The {@link JoinFeatureSet} {@code "left"} and {@code "right"} properties will never be null.
          */
-        INNER,
+        INNER(false, false),
 
         /**
-         * A least left side must have a value to be included.
+         * All records from the left side will be included. If there is no matching feature on the right side,
+         * then the {@link JoinFeatureSet} {@code "right"} property will be {@code null}.
          */
-        LEFT_OUTER,
+        LEFT_OUTER(true, false),
 
         /**
-         * A least right side must have a value to be included.
+         * All records from the right side will be included. If there is no matching feature on the left side,
+         * then the {@link JoinFeatureSet} {@code "left"} property will be {@code null}.
          */
-        RIGHT_OUTER
-    }
+        RIGHT_OUTER(true, true);
 
-    private final FeatureSet left;
-    private final FeatureSet right;
-    private String leftAlias;
-    private String rightAlias;
-    private final Type joinType;
-    private final PropertyIsEqualTo condition;
+        /**
+         * Whether to include all "main" feature instances even if there is no match in the other side.
+         * This is {@code true} for outer joins and {@code false} for inner joins.
+         */
+        final boolean isOuterJoin;
 
-    private final FilterFactory factory;
+        /**
+         * {@code true} if the "main" side is the right side instead than the left side.
+         * See {@link JoinFeatureSet.Iterator} for a definition of "main side".
+         */
+        final boolean swapSides;
 
-    //cache
-    private FeatureType type;
+        /**
+         * Creates an enumeration.
+         */
+        private Type(final boolean isOuterJoin, final boolean swapSides) {
+            this.isOuterJoin = isOuterJoin;
+            this.swapSides   = swapSides;
+        }
 
+        /**
+         * Returns the minimum occurrences for properties on the left or right side.
+         *
+         * @param right  {@code false} for the left side, or {@code true} for the right side.
+         */
+        final int minimumOccurs(final boolean right) {
+            return !isOuterJoin | (swapSides == right) ? 1 : 0;
+        }
 
-    public JoinFeatureSet(final WarningListeners<DataStore> listeners, FeatureSet left,
-            String leftAlias, FeatureSet right, String rightAlias, Type joinType, PropertyIsEqualTo condition)
-    {
-        super(listeners);
-        this.left = left;
-        this.right = right;
-        this.leftAlias = leftAlias;
-        this.rightAlias = rightAlias;
-        this.joinType = joinType;
-        this.condition = condition;
-        factory = new DefaultFilterFactory();
+        /**
+         * Returns the enumeration value for the given characteristics.
+         */
+        static Type valueOf(final boolean isOuterJoin, final boolean swapSides) {
+            return isOuterJoin ? (swapSides ? RIGHT_OUTER : LEFT_OUTER) : INNER;
+        }
     }
 
     /**
-     * Gets the join condition.
-     *
-     * @return Filter
+     * The type of features included in this set. Contains two associations as described in class javadoc.
      */
-    public PropertyIsEqualTo getJoinCondition() {
-        return condition;
-    }
+    private final FeatureType type;
+
+    /**
+     * The first source of features.
+     */
+    public final FeatureSet left;
 
     /**
-     * Gets the join type.
+     * The second source of features.
+     */
+    public final FeatureSet right;
+
+    /**
+     * Name of the associations to the {@link #left} features.
+     * This may be the name of the {@link #left} feature type, but not necessarily.
+     */
+    private final String leftName;
+
+    /**
+     * Name of the associations to the {@link #right} features.
+     * This may be the name of the {@link #right} feature type, but not necessarily.
+     */
+    private final String rightName;
+
+    /**
+     * {@code true} if the "main" side is the right side instead than the left side.
+     * See {@link JoinFeatureSet.Iterator} for a definition of "main side".
+     */
+    private final boolean swapSides;
+
+    /**
+     * Whether to include all "main" feature instances even if there is no match in the other side.
+     * This is {@code true} for outer joins and {@code false} for inner joins.
+     */
+    private final boolean isOuterJoin;
+
+    /**
+     * The join condition in the form <var>property from left feature</var> = <var>property from right feature</var>.
+     * This condition specifies also if the comparison is {@linkplain PropertyIsEqualTo#isMatchingCase() case sensitive}
+     * and {@linkplain PropertyIsEqualTo#getMatchAction() how to compare multi-values}.
+     */
+    public final PropertyIsEqualTo condition;
+
+    /**
+     * The factory to use for creating {@code Query} expressions for retrieving subsets of feature sets.
+     */
+    private final FilterFactory factory;
+
+    /**
+     * Creates a new feature set joining the two given sets. The {@code featureInfo} map defines the name,
+     * description or other information for the {@code FeatureType} created by this method. It can contain all
+     * the properties described in {@link org.apache.sis.feature.DefaultFeatureType} plus the following ones:
+     *
+     * <ul>
+     *   <li>{@code "identifierDelimiter"} — string to insert between left and right identifiers in the identifiers
+     *     generated by the join operation. If this property is not specified, then no identifier will be generated.</li>
+     *   <li>{@code "identifierPrefix"} — string to insert at the beginning of join identifiers (optional).</li>
+     *   <li>{@code "identifierSuffix"} — string to insert at the end of join identifiers (optional).</li>
+     * </ul>
      *
-     * @return join type
+     * @param  listeners    the set of registered warning listeners for the data store, or {@code null} if none.
+     * @param  left         the first source of features. This is often (but not necessarily) the largest set.
+     * @param  leftAlias    name of the associations to the {@code left} features, or {@code null} for a default name.
+     * @param  right        the second source of features. Should be the set in which iterations are cheapest.
+     * @param  rightAlias   name of the associations to the {@code right} features, or {@code null} for a default name.
+     * @param  joinType     whether values on both sides are required (inner join), or only one side (outer join).
+     * @param  condition    join condition as <var>property from left feature</var> = <var>property from right feature</var>.
+     * @param  featureInfo  information about the {@link FeatureType} of this
+     * @throws DataStoreException if an error occurred while creating the feature set.
      */
-    public Type getJoinType() {
-        return joinType;
+    public JoinFeatureSet(final WarningListeners<DataStore> listeners,
+                          final FeatureSet left,  String leftAlias,
+                          final FeatureSet right, String rightAlias,
+                          final Type joinType, final PropertyIsEqualTo condition,
+                          Map<String,?> featureInfo)
+            throws DataStoreException
+    {
+        super(listeners);
+        final FeatureType leftType  = left.getType();
+        final FeatureType rightType = right.getType();
+        final GenericName leftName  = leftType.getName();
+        final GenericName rightName = rightType.getName();
+        if (leftAlias  == null) leftAlias  = leftName.toString();
+        if (rightAlias == null) rightAlias = rightName.toString();
+        this.left        = left;
+        this.right       = right;
+        this.leftName    = leftAlias;
+        this.rightName   = rightAlias;
+        this.swapSides   = joinType.swapSides;
+        this.isOuterJoin = joinType.isOuterJoin;
+        this.condition   = condition;
+        this.factory     = new DefaultFilterFactory();       // TODO: replace by some static instance?
+        /*
+         * We could build the FeatureType only when first needed, but the type is required by the iterators.
+         * Since we are going to need the type for any use of this JoinFeatureSet, better to create it now.
+         */
+        PropertyType[] properties = new PropertyType[] {
+            new DefaultAssociationRole(name(leftAlias),  leftType,  joinType.minimumOccurs(false), 1),
+            new DefaultAssociationRole(name(rightAlias), rightType, joinType.minimumOccurs(true),  1)
+        };
+        final String identifierDelimiter = Containers.property(featureInfo, "identifierDelimiter", String.class);
+        if (identifierDelimiter != null && AttributeConvention.hasIdentifier(leftType)
+                                        && AttributeConvention.hasIdentifier(rightType))
+        {
+            final Operation identifier = FeatureOperations.compound(
+                    name(AttributeConvention.IDENTIFIER_PROPERTY), identifierDelimiter,
+                    Containers.property(featureInfo, "identifierPrefix", String.class),
+                    Containers.property(featureInfo, "identifierSuffix", String.class), properties);
+            properties = ArraysExt.insert(properties, 0, 1);
+            properties[0] = identifier;
+        }
+        if (featureInfo == null) {
+            featureInfo = name(leftName.tip().toString() + '-' + rightName.tip());
+        }
+        type = new DefaultFeatureType(featureInfo, false, null, properties);
     }
 
     /**
-     * Gets the left feature source.
-     *
-     * @return left join feature set
+     * Creates a minimal {@code properties} map for feature type or property type constructors.
+     * This minimalist map contain only the mandatory entry, which is the name.
      */
-    public FeatureSet getLeft() {
-        return left;
+    private static Map<String,?> name(final Object name) {
+        return Collections.singletonMap(DefaultFeatureType.NAME_KEY, name);
     }
 
     /**
-     * Gets the right feature source.
+     * Specifies whether values on both sides are required (inner join), or only one side (outer join).
      *
-     * @return right join feature set
+     * @return whether values on both sides are required (inner join), or only one side (outer join).
      */
-    public FeatureSet getRight() {
-        return right;
+    public Type getJoinType() {
+        return Type.valueOf(isOuterJoin, swapSides);
     }
 
+    /**
+     * Returns a description of properties that are common to all features in this dataset.
+     * This type may contain one identifier and always contains two associations,
+     * to the {@linkplain #left} and {@link #right} set of features respectively.
+     *
+     * @return a description of properties that are common to all features in this dataset.
+     */
     @Override
-    public FeatureType getType() throws DataStoreException {
-        if (type == null) {
-            final FeatureType leftType = left.getType();
-            final FeatureType rightType = right.getType();
-            final GenericName leftName = leftType.getName();
-            final GenericName rightName = rightType.getName();
-            if (leftAlias == null) leftAlias = leftName.toString();
-            if (rightAlias == null) rightAlias = rightName.toString();
-
-            final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
-            ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY).setDefaultValue("");
-            ftb.addAssociation(leftType).setName(leftAlias).setMinimumOccurs(0).setMaximumOccurs(1);
-            ftb.addAssociation(rightType).setName(rightAlias).setMinimumOccurs(0).setMaximumOccurs(1);
-            ftb.setName(leftName.tip().toString() + '-' + rightName.tip().toString());
-            type = ftb.build();
-        }
+    public FeatureType getType() {
         return type;
     }
 
-    @Override
-    public Metadata getMetadata() throws DataStoreException {
-        final FeatureType type = getType();
-        final DefaultMetadata metadata = new DefaultMetadata();
-        final DefaultDataIdentification ident = new DefaultDataIdentification();
-        final DefaultCitation citation = new DefaultCitation();
-        citation.setTitle(new SimpleInternationalString(type.getName().toString()));
-        citation.setIdentifiers(Arrays.asList(new NamedIdentifier(type.getName())));
-        ident.setCitation(citation);
-        metadata.setIdentificationInfo(Arrays.asList(ident));
-        return metadata;
-    }
-
     /**
-     * Envelope is not stored or computed.
+     * Returns {@code null} since computing an envelope would be costly for this set.
      *
-     * @return always null
-     * @throws DataStoreException
+     * @return always {@code null} in default implementation.
+     *
+     * @todo Revisit the method contract by allowing the envelope to be only an estimation, potentially larger.
      */
     @Override
-    public Envelope getEnvelope() throws DataStoreException {
+    public Envelope getEnvelope() {
         return null;
     }
 
+    /**
+     * Returns a stream of all features contained in this dataset.
+     *
+     * @param  parallel  {@code true} for a parallel stream (if supported), or {@code false} for a sequential stream.
+     * @return all features contained in this dataset.
+     * @throws DataStoreException if an error occurred while creating the stream.
+     */
     @Override
-    public Stream<Feature> features(boolean parallel) throws DataStoreException {
-        final JoinIterator ite;
-        switch (joinType) {
-            case INNER :       ite = new JoinInnerRowIterator(); break;
-            case LEFT_OUTER :  ite = new JoinOuterRowIterator(true); break;
-            case RIGHT_OUTER : ite = new JoinOuterRowIterator(false); break;
-            default:
-                throw new IllegalArgumentException("Unknown Join type : " + joinType);
-        }
-        final Stream<Feature> stream = StreamSupport.stream(
-            Spliterators.spliteratorUnknownSize(ite, Spliterator.ORDERED),
-            false);
-        stream.onClose(ite::close);
-        return stream;
+    public Stream<Feature> features(final boolean parallel) throws DataStoreException {
+        final Iterator it = new Iterator();
+        return StreamSupport.stream(it, parallel).onClose(it);
     }
 
     /**
-     * Aggregate all feature from selectors to a single complex feature.
-     *
-     * @return aggregated features
-     * @throws DataStoreException
+     * Creates a new features containing an association to the two given features.
+     * The {@code main} feature can not be null (this is not verified).
      */
-    private Feature toFeature(final Feature left, final Feature right) throws DataStoreException{
-        final FeatureType type = getType(); //force creating type.
-        final Feature f = type.newInstance();
-
-        String id = "";
-
-        if (left != null) {
-            id += left.getPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString());
-            f.setPropertyValue(leftAlias,left);
-        }
-        if (right != null) {
-            if (left != null) id += " ";
-            id += right.getPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString());
-            f.setPropertyValue(rightAlias,right);
+    private Feature join(Feature main, Feature filtered) {
+        if (swapSides) {
+            final Feature t = main;
+            main = filtered;
+            filtered = t;
         }
-        f.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), id);
+        final Feature f = type.newInstance();
+        f.setPropertyValue(leftName,  main);
+        f.setPropertyValue(rightName, filtered);
         return f;
     }
 
-    private interface JoinIterator extends Iterator<Feature>, AutoCloseable {
+    /**
+     * Iterator over the features resulting from the inner or outer join operation.
+     * The {@link #run()} method disposes the resources.
+     */
+    private final class Iterator implements Spliterator<Feature>, Consumer<Feature>, Runnable {
+        /**
+         * The main stream or a split iterator to close when the {@link #run()} method will be invoked.
+         * This is initially the stream from which {@link #mainIterator} has been created. However if
+         * {@link #trySplit()} has been invoked, then this handler may be the other {@code Iterator}
+         * instance which itself contains a reference to the stream to close, thus forming a chain.
+         */
+        private Runnable mainCloseHandler;
 
-        @Override
-        public default void close() {}
+        /**
+         * An iterator over all features in the "main" (usually left) side. The "main" side is the side which
+         * may include all features: in a "left outer join" this is the left side, and in a "right outer join"
+         * this is the right side. For inner join we arbitrarily take the left side in accordance with public
+         * class javadoc, which suggests to put the most costly or larger set on the left side.
+         *
+         * <p>Only one iteration will be performed on those features, contrarily to the other side where we may
+         * iterate over the same elements many times.</p>
+         */
+        private final Spliterator<Feature> mainIterator;
 
-    }
+        /**
+         * A feature fetched from the {@link #mainIterator}. The join operation will match this feature with
+         * zero, one or more features from the other side. A {@code null} value means that this feature needs
+         * to be retrieved with {@code mainIterator.tryAdvance(…)}.
+         */
+        private Feature mainFeature;
 
-    /**
-     * Iterate on both collections with an Inner join condition.
-     */
-    private class JoinInnerRowIterator implements JoinIterator {
+        /**
+         * The stream over features in the other (usually right) side. A new stream will be created every time a new
+         * feature from the main side is processed. For this reason, it should be the cheapest stream if possible.
+         */
+        private Stream<Feature> filteredStream;
+
+        /**
+         * Iterator for the {@link #filteredStream}. A new iterator will be recreated every time a new feature
+         * from the main side is processed.
+         */
+        private Spliterator<Feature> filteredIterator;
+
+        /**
+         * A feature fetched from the {@link #filteredIterator}, or {@code null} if none.
+         */
+        private Feature filteredFeature;
 
-        private final Stream<Feature> leftStream;
-        private final Iterator<Feature> leftIterator;
-        private Stream<Feature> rightStream;
-        private Iterator<Feature> rightIterator;
-        private Feature leftFeature;
-        private Feature combined;
-
-        JoinInnerRowIterator() throws DataStoreException {
-            leftStream = left.features(false);
-            leftIterator = leftStream.iterator();
+        /**
+         * Creates a new iterator. We do not use parallelized {@code mainStream} here because the {@code accept(…)}
+         * methods used by this {@code Iterator} can not be invoked concurrently by different threads. It does not
+         * present parallelization at a different level since this {@code Iterator} supports {@link #trySplit()},
+         * so the {@link Stream} wrapping it can use parallelization.
+         */
+        Iterator() throws DataStoreException {
+            final Stream<Feature> mainStream = (swapSides ? right : left).features(false);
+            mainCloseHandler = mainStream::close;
+            mainIterator = mainStream.spliterator();
         }
 
-        @Override
-        public Feature next() {
-            try {
-                searchNext();
-            } catch (DataStoreException ex) {
-                throw new BackingStoreException(ex);
-            }
-            Feature f = combined;
-            combined = null;
-            return f;
+        /**
+         * Creates an iterator resulting from the call to {@link #trySplit()}.
+         */
+        private Iterator(final Spliterator<Feature> it) {
+            mainIterator = it;
         }
 
+        /**
+         * If this iterator can be partitioned, returns a spliterator covering a prefix of the feature set.
+         * Upon return from this method, this iterator will cover a suffix of the feature set.
+         * Returns {@code null} if this iterator can not be partitioned.
+         */
         @Override
-        public void close() {
-            leftStream.close();
-            if (rightStream != null) {
-                rightStream.close();
-            }
+        public Spliterator<Feature> trySplit() {
+            final Spliterator<Feature> s = mainIterator.trySplit();
+            if (s == null) {
+                return null;
+            }
+            final Iterator it = new Iterator(s);
+            it.mainCloseHandler = mainCloseHandler;
+            mainCloseHandler = it;
+            return it;
         }
 
+        /**
+         * Specifies that the iterator will return only non-null elements. Whether those elements will
+         * be ordered depends on whether the main iterator provides ordered elements in the first place.
+         *
+         * <p><b>NOTE:</b> to be strict, we should check if the "filtered" stream is also ordered. But this
+         * is more difficult to check. Current implementation assumes that if the "mean" stream is ordered,
+         * then the other stream is ordered too. Furthermore the {@link #trySplit()} method works only on
+         * the main stream, so at least the {@code trySplit} requirement about prefix and suffix order is
+         * still fulfill even if the other stream is unordered.</p>
+         */
         @Override
-        public boolean hasNext() {
-            try {
-                searchNext();
-            } catch (DataStoreException ex) {
-                throw new BackingStoreException(ex);
-            }
-            return combined != null;
+        public int characteristics() {
+            return (mainIterator.characteristics() & ORDERED) | NONNULL;
         }
 
-        private void searchNext() throws DataStoreException {
-            if (combined != null) return;
-
-            final PropertyIsEqualTo equal = getJoinCondition();
-            final Expression leftProperty = equal.getExpression1();
-            final Expression rightProperty = equal.getExpression2();
+        /**
+         * Estimated size is unknown.
+         */
+        @Override
+        public long estimateSize() {
+            return Long.MAX_VALUE;
+        }
 
-            //we might have several right features for one left
-            if (leftFeature != null && rightIterator != null) {
-                while (combined == null && rightIterator.hasNext()) {
-                    final Feature rightRes = rightIterator.next();
-                    combined = toFeature(leftFeature, rightRes);
-                }
-            }
-            if (rightIterator != null && !rightIterator.hasNext()){
-                //no more results in right iterator, close iterator
-                rightStream.close();
-                rightStream = null;
-                rightIterator = null;
-            }
-            while (combined == null && leftIterator.hasNext()) {
-                leftFeature = leftIterator.next();
-                final Object leftValue = leftProperty.evaluate(leftFeature);
-                if (rightIterator == null) {
-                    final SimpleQuery rightQuery = new SimpleQuery();
-                    rightQuery.setFilter(factory.equal(rightProperty, factory.literal(leftValue), true, MatchAction.ALL));
-                    rightStream = right.subset(rightQuery).features(false);
-                    rightIterator = rightStream.iterator();
-                }
-                while (combined == null && rightIterator.hasNext()) {
-                    final Feature rightRow = rightIterator.next();
-                    combined = toFeature(leftFeature, rightRow);
-                }
-                if (combined == null) {
-                    //no more results in right iterator, close iterator
-                    rightStream.close();
-                    rightStream = null;
-                    rightIterator = null;
-                }
+        /**
+         * Closes the streams used by this iterator, together with the streams used by any spliterator
+         * created by {@link #trySplit()}. This method is registered to {@link Stream#onClose(Runnable)}.
+         */
+        @Override
+        public void run() {
+            closeFilteredIterator();
+            final Runnable toClose = mainCloseHandler;
+            if (toClose != null) {
+                mainCloseHandler = null;            // Cleared first in case of error.
+                toClose.run();
             }
         }
-    }
 
-    /**
-     * Iterate on both collections with an outer join condition.
-     */
-    private class JoinOuterRowIterator implements JoinIterator {
-
-        private final Stream<Feature> primeStream;
-        private final Iterator<Feature> primeIterator;
-        private Stream<Feature> secondStream;
-        private Iterator<Feature> secondIterator;
-
-        private final boolean leftJoint;
-        private Feature primeFeature;
-        private Feature nextFeature;
-
-        JoinOuterRowIterator(final boolean leftJoint) throws DataStoreException{
-            this.leftJoint = leftJoint;
-            if (leftJoint) {
-                primeStream = left.features(false);
-            } else {
-                primeStream = right.features(false);
+        /**
+         * Invoked when iteration on the filtered stream ended, before to move on the next feature of the main stream.
+         * This method is idempotent: it has no effect if the stream is already closed.
+         */
+        private void closeFilteredIterator() {
+            final Stream<Feature> stream = filteredStream;
+            filteredStream   = null;                // Cleared before call to close() in case of error.
+            filteredIterator = null;
+            filteredFeature  = null;                // Used as a sentinel value by this.forEachRemaining(…).
+            mainFeature      = null;                // Indicate that we will need to advance in mainIterator.
+            if (stream != null) {
+                stream.close();
             }
-            primeIterator = primeStream.iterator();
         }
 
-        @Override
-        public Feature next() {
+        /**
+         * Creates a new iterator over the filtered set of features (usually the right side).
+         * The filtering condition is determined by the current {@link #mainFeature}.
+         */
+        private void createFilteredIterator() {
+            Expression expression1 = condition.getExpression1();
+            Expression expression2 = condition.getExpression2();
+            FeatureSet filteredSet = right;
+            if (swapSides) {
+                filteredSet  = left;
+                Expression t = expression2;
+                expression2  = expression1;
+                expression1  = t;
+            }
+            final Object mainValue = expression1.evaluate(mainFeature);
+            final Filter filter;
+            if (mainValue != null) {
+                filter = factory.equal(expression2, factory.literal(mainValue),
+                            condition.isMatchingCase(), condition.getMatchAction());
+            } else {
+                filter = factory.isNull(expression2);
+            }
+            final SimpleQuery query = new SimpleQuery();
+            query.setFilter(filter);
             try {
-                searchNext();
-            } catch (DataStoreException ex) {
-                throw new BackingStoreException(ex);
+                filteredStream = filteredSet.subset(query).features(false);
+            } catch (DataStoreException e) {
+                throw new BackingStoreException(e);
             }
-            Feature f = nextFeature;
-            nextFeature = null;
-            return f;
+            filteredIterator = filteredStream.spliterator();
         }
 
+        /**
+         * Executes the given action on all remaining features in the {@code JoinFeatureSet}.
+         */
         @Override
-        public void close() {
-            primeStream.close();
-            if (secondStream != null) {
-                secondStream.close();
-            }
+        public void forEachRemaining(final Consumer<? super Feature> action) {
+            final Consumer<Feature> forFiltered = (final Feature feature) -> {
+                if (feature != null) {
+                    action.accept(join(mainFeature, filteredFeature = feature));
+                }
+            };
+            final Consumer<Feature> forMain = (final Feature feature) -> {
+                if (feature != null) {
+                    mainFeature = feature;
+                    createFilteredIterator();
+                    filteredIterator.forEachRemaining(forFiltered);
+                    final boolean none = (filteredFeature == null);
+                    closeFilteredIterator();
+                    if (none && isOuterJoin) {
+                        action.accept(join(feature, null));
+                    }
+                    // Do not close the main stream since it may be in use by other Spliterators.
+                }
+            };
+            forMain.accept(mainFeature);                // In case some 'tryAdvance' has been invoked before.
+            mainIterator.forEachRemaining(forMain);
         }
 
+        /**
+         * Callback for {@code Spliterator.tryAdvance(this)} on {@link #filteredIterator}.
+         * Used by {@link #tryAdvance(Consumer)} implementation only.
+         */
         @Override
-        public boolean hasNext() {
-            try {
-                searchNext();
-            } catch (DataStoreException ex) {
-                throw new BackingStoreException(ex);
-            }
-            return nextFeature != null;
+        public void accept(final Feature feature) {
+            filteredFeature = feature;
         }
 
-        private void searchNext() throws DataStoreException {
-            if (nextFeature != null) return;
-
-            final PropertyIsEqualTo equal = getJoinCondition();
-            final Expression leftProperty = equal.getExpression1();
-            final Expression rightProperty = equal.getExpression2();
-
-            //we might have several right features for one left
-            if (primeFeature != null && secondIterator != null) {
-                while (nextFeature == null && secondIterator.hasNext()) {
-                    final Feature secondCandidate = secondIterator.next();
-                    nextFeature = checkValid(primeFeature, secondCandidate, leftJoint);
-                }
-            }
-            while (nextFeature == null && primeIterator.hasNext()) {
-                primeFeature = primeIterator.next();
-                if (secondIterator != null) {
-                    secondStream.close();
-                    secondStream = null;
-                    secondIterator = null;
+        /**
+         * Executes the given action on the next feature in the {@code JoinFeatureSet}.
+         */
+        @Override
+        public boolean tryAdvance​(final Consumer<? super Feature> action) {
+            for (;;) {
+                if (mainFeature == null) {
+                    do if (!mainIterator.tryAdvance(this)) {
+                        return false;
+                    } while (filteredFeature == null);
+                    mainFeature = filteredFeature;
+                    filteredFeature = null;
                 }
-                final Object primeValue;
-                if (leftJoint) {
-                    primeValue = leftProperty.evaluate(primeFeature);
-                } else {
-                    primeValue = rightProperty.evaluate(primeFeature);
+                if (filteredIterator == null) {
+                    createFilteredIterator();
                 }
-                if (secondIterator == null) {
-                    final SimpleQuery query = new SimpleQuery();
-                    if (leftJoint) {
-                        query.setFilter(factory.equal(rightProperty, factory.literal(primeValue), true, MatchAction.ALL));
-                        secondStream = right.subset(query).features(false);
-                        secondIterator = secondStream.iterator();
-                    } else {
-                        query.setFilter(factory.equal(leftProperty, factory.literal(primeValue), true, MatchAction.ALL));
-                        secondStream = left.subset(query).features(false);
-                        secondIterator = secondStream.iterator();
+                final boolean none = (filteredFeature == null);
+                while (filteredIterator.tryAdvance(this)) {
+                    if (filteredFeature != null) {
+                        action.accept(join(mainFeature, filteredFeature));
+                        return true;
                     }
                 }
-                while (nextFeature == null && secondIterator.hasNext()) {
-                    final Feature rightRow = secondIterator.next();
-                    nextFeature = checkValid(primeFeature, rightRow,leftJoint);
-                }
-                if (nextFeature == null) {
-                    //outer left effect, no right match but still we must return the left side
-                    if (leftJoint) {
-                        nextFeature = toFeature(primeFeature,null);
-                    } else {
-                        nextFeature = toFeature(null,primeFeature);
-                    }
+                final Feature feature = mainFeature;
+                closeFilteredIterator();
+                if (none && isOuterJoin) {
+                    action.accept(join(feature, null));
+                    return true;
                 }
             }
         }
-
-        private Feature checkValid(final Feature left, final Feature right, final boolean leftJoin) throws DataStoreException{
-            final Feature candidate;
-            if (leftJoin) {
-                candidate = toFeature(left,right);
-            } else {
-                candidate = toFeature(right,left);
-            }
-            return candidate;
-        }
     }
 }



Mime
View raw message