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 4cc6c97337 Refactor `TimeMethods` with more support of conversions of
`java.time` objects to compare. Add in `TemporalFilter` an optimization for
temporal objects similar to the optimization already done for numerical objects.
4cc6c97337 is described below
commit 4cc6c97337065510f1762f17a8509adab1ad3621
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Jan 26 01:14:31 2026 +0100
Refactor `TimeMethods` with more support of conversions of `java.time`
objects to compare.
Add in `TemporalFilter` an optimization for temporal objects similar to the
optimization already done for numerical objects.
---
.../org/apache/sis/filter/ComparisonFilter.java | 104 ++++-
.../main/org/apache/sis/filter/TemporalFilter.java | 9 +-
.../org/apache/sis/filter/TemporalOperation.java | 2 +-
.../sis/filter/base/BinaryFunctionWidening.java | 3 +-
.../apache/sis/filter/base/ConvertFunction.java | 12 +-
.../main/org/apache/sis/filter/base/Node.java | 12 +
.../org/apache/sis/temporal/DefaultInstant.java | 6 +-
.../main/org/apache/sis/temporal/TimeMethods.java | 446 ++++++++++++---------
.../org/apache/sis/storage/FeatureQueryTest.java | 41 +-
9 files changed, 416 insertions(+), 219 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
index 50da8c794e..471fd683fd 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/ComparisonFilter.java
@@ -19,9 +19,13 @@ package org.apache.sis.filter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.DateTimeException;
+import java.time.temporal.Temporal;
+import java.util.Date;
import java.util.List;
import java.util.Collection;
import java.util.Objects;
+import java.util.function.BiPredicate;
+import org.opengis.util.CodeList;
import org.apache.sis.math.Fraction;
import org.apache.sis.filter.base.Node;
import org.apache.sis.filter.base.BinaryFunctionWidening;
@@ -152,6 +156,102 @@ abstract class ComparisonFilter<R> extends
BinaryFunctionWidening<R, Object, Obj
return false;
}
+ /**
+ * Tries to optimize this filter. Fist, this method applies the
optimization documented
+ * in the {@linkplain Optimization.OnFilter#optimize default method
impmementation}.
+ * Then, if it is possible to avoid to inspect the number types every time
that the
+ * filter is evaluated, this method returns a more direct implementation.
+ *
+ * @param optimization the simplifications or optimizations to apply on
this filter.
+ * @return the simplified or optimized filter, or {@code this} if no
optimization has been applied.
+ */
+ @Override
+ public final Filter<R> optimize(final Optimization optimization) {
+ final Filter<R> result =
Optimization.OnFilter.super.optimize(optimization);
+ if (result instanceof ComparisonFilter<?>) {
+ final var optimized = (ComparisonFilter<R>) result;
+ final var numeric = optimized.new Numeric();
+ if (numeric.evaluator != null) {
+ return numeric;
+ }
+ final Class<?> type1, type2;
+ if (isTemporal(type1 = getResultClass(expression1)) &&
isTemporal(type2 = getResultClass(expression2))) {
+ final TimeMethods<?> methods = TimeMethods.forTypes(type1,
type2);
+ if (methods != null) {
+ return optimized.new
Time(methods.predicate(temporalTest(), type2));
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns whether the given type is considered temporal for the purpose
of the comparison filter.
+ * This is necessary for avoiding the comparable objects such as {@link
String} are wrongly handled
+ * by {@link TimeMethods}.
+ */
+ private static boolean isTemporal(final Class<?> type) {
+ return (type != null) && (Temporal.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type));
+ }
+
+ /**
+ * An optimized versions of this filter for the case where the operands
are numeric.
+ */
+ private final class Numeric extends Node implements Filter<R> {
+ /** For cross-version compatibility during (de)serialization. */
+ private static final long serialVersionUID = 4969425622445580192L;
+
+ /** The expression which performs the comparison and returns the
result as an integer. */
+ @SuppressWarnings("serial") final Expression<R, ? extends Number>
evaluator;
+
+ /** Creates a new filter. Callers must verifies that {@link
#evaluator} is non-null. */
+ Numeric() {evaluator = specialize();}
+
+ /** Delegates to the enclosing class.*/
+ @Override public CodeList<?> getOperatorType() {return
ComparisonFilter.this.getOperatorType();}
+ @Override public Class<? super R> getResourceClass() {return
ComparisonFilter.this.getResourceClass();}
+ @Override public List<Expression<R,?>> getExpressions() {return
ComparisonFilter.this.getExpressions();}
+ @Override protected Collection<?> getChildren() {return
ComparisonFilter.this.getChildren();}
+
+ /** Determines if the test represented by this filter passes with the
given operands. */
+ @Override public boolean test(final R candidate) {
+ return ((Integer) evaluator.apply(candidate)) != 0;
+ }
+ }
+
+ /**
+ * An optimized versions of this filter for the case where the operands
are temporal.
+ */
+ private final class Time extends Node implements Filter<R> {
+ /** For cross-version compatibility during (de)serialization. */
+ private static final long serialVersionUID = -5132906457258846016L;
+
+ /** The function which performs the comparisons. */
+ @SuppressWarnings("serial") final BiPredicate<?,?> evaluator;
+
+ /** Creates a new filter. */
+ Time(final BiPredicate<?,?> evaluator) {this.evaluator = evaluator;}
+
+ /** Delegates to the enclosing class.*/
+ @Override public CodeList<?> getOperatorType() {return
ComparisonFilter.this.getOperatorType();}
+ @Override public Class<? super R> getResourceClass() {return
ComparisonFilter.this.getResourceClass();}
+ @Override public List<Expression<R,?>> getExpressions() {return
ComparisonFilter.this.getExpressions();}
+ @Override protected Collection<?> getChildren() {return
ComparisonFilter.this.getChildren();}
+
+ /** Determines if the test represented by this filter passes with the
given operands. */
+ @SuppressWarnings("unchecked")
+ @Override public boolean test(final R candidate) {
+ final Object left = expression1.apply(candidate);
+ if (left != null) {
+ final Object right = expression2.apply(candidate);
+ if (right != null) {
+ return ((BiPredicate) evaluator).test(left, right);
+ }
+ }
+ 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,
@@ -240,8 +340,8 @@ abstract class ComparisonFilter<R> extends
BinaryFunctionWidening<R, Object, Obj
if (r != null) return r.intValue() != 0;
}
try {
- Boolean t = TimeMethods.compareIfTemporal(temporalTest(), left,
right);
- if (t != null) return t;
+ Boolean c = TimeMethods.compareIfTemporal(temporalTest(), left,
right);
+ if (c != null) return c;
} catch (DateTimeException e) {
warning(e);
return false;
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
index c696cb2815..e86a63c9a8 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalFilter.java
@@ -75,7 +75,8 @@ class TemporalFilter<R,T> extends BinaryFunction<R,T,T>
}
/**
- * Creates a new temporal function.
+ * Creates a new temporal filter. This is the implementation of {@link
DefaultFilterFactory} methods
+ * which create {@link TemporalOperator} instances.
*
* @param <R> the type of resources (e.g. {@code Feature}) used
as inputs.
* @param <v> compile-type value of the {@code type} argument.
@@ -114,9 +115,9 @@ class TemporalFilter<R,T> extends BinaryFunction<R,T,T>
* Creations of `TemporalFilter` instances below are safe because
`TimeMethods.type` is a parent
* of both `expression1` and `expression2` value types (verified by
assertions). Therefore, with
* `commonType` of type `Class<T>` no matter if <T> is a super-type or
a sub-type of <V>, we can
- * assert that the parmeterized type of the two expressions is `<?
extends T>`.
+ * assert that the parameterized type of the two expressions is `<?
extends T>`.
*/
- final TemporalOperation<?> operation =
factory.create(TimeMethods.find(commonType)).unique();
+ final TemporalOperation<?> operation =
factory.create(TimeMethods.forType(commonType)).unique();
assert operation.comparators.type.isAssignableFrom(commonType) :
commonType;
assert commonType.isAssignableFrom(c1) : c1;
assert commonType.isAssignableFrom(c2) : c2;
@@ -124,7 +125,7 @@ class TemporalFilter<R,T> extends BinaryFunction<R,T,T>
// Safe because `commonType` extends both Period and T.
return new Periods(operation, expression1, expression2);
}
- if (operation.comparators.isDynamic()) {
+ if (operation.comparators.isDynamic) {
return new TemporalFilter(operation, expression1, expression2);
}
return new Instants(operation, expression1, expression2);
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
index 143560c721..ff6fa3905a 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/TemporalOperation.java
@@ -205,7 +205,7 @@ abstract class TemporalOperation<T> implements Serializable
{
position = other.getPosition();
}
if (position != null) {
- return comparators.compare(test, self, position);
+ return comparators.convertAndCompare(test, self, position);
}
}
return false;
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunctionWidening.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunctionWidening.java
index e3e69504f3..13d1e2eb9a 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunctionWidening.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/BinaryFunctionWidening.java
@@ -116,8 +116,7 @@ public abstract class BinaryFunctionWidening<R, A1, A2>
extends BinaryFunction<R
* @see FeatureExpression#getResultClass()
*/
private static NumberType getNumberType(final Expression<?,?> expression) {
- return (expression instanceof FeatureExpression<?,?>)
- ? NumberType.forClass(((FeatureExpression<?,?>)
expression).getResultClass()).orElse(NumberType.NULL) : NumberType.NULL;
+ return
NumberType.forClass(getResultClass(expression)).orElse(NumberType.NULL);
}
/**
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/ConvertFunction.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/ConvertFunction.java
index 10ef96a8e9..6f4a5f1a0a 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/ConvertFunction.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/ConvertFunction.java
@@ -97,14 +97,14 @@ public final class ConvertFunction<R,S,V> extends
UnaryFunction<R,S>
@SuppressWarnings({"unchecked", "rawtypes"})
public Expression<R,V> recreate(Expression<R,?>[] effective) {
final Expression<R,?> e = effective[0];
- if (e instanceof FeatureExpression<?,?>) {
- final Class<? extends V> target = getResultClass();
// This is <V>.
- final Class<?> source = ((FeatureExpression<?,?>)
e).getResultClass(); // May become <S>.
- if (target.isAssignableFrom(source)) {
+ final Class<?> result = getResultClass(e); // May
become <S>.
+ if (result != null) {
+ final Class<? extends V> target = getResultClass(); // This is
<V>.
+ if (target.isAssignableFrom(result)) {
return (Expression<R,V>) e;
}
- if (source != Object.class) {
- return new ConvertFunction(e, source, target);
+ if (result != Object.class) {
+ return new ConvertFunction(e, result, target);
}
}
final Class<? super S> source = converter.getSourceClass();
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/Node.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/Node.java
index 6b78d84819..62e7c65fd8 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/Node.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/filter/base/Node.java
@@ -122,6 +122,18 @@ public abstract class Node implements Serializable {
return null;
}
+ /**
+ * Returns the type of values computed by the given expression.
+ *
+ * @param <V> compile-time type of values.
+ * @param expression the expression for which to get the runtime type of
values.
+ * @return the type of values computed by the given expression.
+ */
+ protected static <V> Class<? extends V> getResultClass(final
Expression<?,V> expression) {
+ return (expression instanceof FeatureExpression<?,?>)
+ ? ((FeatureExpression<?,V>) expression).getResultClass() :
null;
+ }
+
/**
* Returns the mathematical symbol for this binary function.
* For comparison operators, the symbol should be one of {@literal < > ≤ ≥
= ≠}.
diff --git
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultInstant.java
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultInstant.java
index 33415a75cd..1392f0b8e2 100644
---
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultInstant.java
+++
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultInstant.java
@@ -241,18 +241,18 @@ cmp: if (canTestBefore | canTestAfter | canTestEqual) {
if (oip == IndeterminateValue.NOW) {
t1 = position;
if (t1 == null) break cmp;
- comparators = TimeMethods.find(t1.getClass());
+ comparators = TimeMethods.forType(t1.getClass());
t2 = comparators.now();
} else {
t2 = other.getPosition();
if (t2 == null) break cmp;
if (indeterminate == IndeterminateValue.NOW) {
- comparators = TimeMethods.find(t2.getClass());
+ comparators = TimeMethods.forType(t2.getClass());
t1 = comparators.now();
} else {
t1 = position;
if (t1 == null) break cmp;
- comparators =
TimeMethods.find(Classes.findCommonClass(t1.getClass(), t2.getClass()));
+ comparators =
TimeMethods.forType(Classes.findCommonClass(t1.getClass(), t2.getClass()));
}
}
// This is where the @SuppressWarnings(…) apply.
diff --git
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java
index 935762c40f..03fde3581c 100644
---
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java
+++
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TimeMethods.java
@@ -21,6 +21,7 @@ import java.util.Date;
import java.util.Calendar;
import java.util.Optional;
import java.util.function.Supplier;
+import java.util.function.Function;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.time.Instant;
@@ -68,21 +69,11 @@ import org.apache.sis.util.resources.Errors;
*
* @author Martin Desruisseaux (Geomatys)
*/
-public class TimeMethods<T> implements Serializable {
- /**
- * For cross-version compatibility.
- */
- private static final long serialVersionUID = 1075289362575825939L;
-
- /**
- * The type of temporal objects accepted by this set of operations.
- */
- public final Class<T> type;
-
+public final class TimeMethods<T> implements Serializable {
/**
* The test to apply: equal, before or after.
*
- * @see #compare(Test, T, TemporalAccessor)
+ * @see #convertAndCompare(Test, T, TemporalAccessor)
*/
public enum Test {
/** Identifies the <var>A</var> = <var>B</var> test. */
@@ -156,6 +147,22 @@ public class TimeMethods<T> implements Serializable {
abstract boolean fromCompareTo(int result);
}
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = 3421610320642857317L;
+
+ /**
+ * The type of temporal objects accepted by this set of operations.
+ */
+ public final Class<T> type;
+
+ /**
+ * Converter from an object of arbitrary class to an object of class
{@code <T>}, or {@code null} if none.
+ * The function may return {@code null} if the given object is an instance
of unsupported type.
+ */
+ public final transient Function<Object, T> converter;
+
/**
* Predicate to execute for testing the ordering between temporal objects.
* This comparison operator differs from the {@code compareTo(…)} method
in that it compares only the
@@ -186,77 +193,76 @@ public class TimeMethods<T> implements Serializable {
/**
* Whether the temporal object have a time zone, explicitly or implicitly.
*/
- private final boolean hasZone;
+ private final transient boolean hasZone;
/**
- * Creates a new set of operators. This method is for subclasses only.
- * For getting a {@code TimeMethods} instance, see {@link #find(Class)}.
+ * Whether the end point will be determined dynamically every time that a
method is invoked.
*/
- private TimeMethods(final Class<T> type,
- final BiPredicate<T,T> isBefore,
- final BiPredicate<T,T> isAfter,
- final BiPredicate<T,T> isEqual,
- final Supplier<T> now,
- final BiFunction<T, ZoneId, Temporal> withZone,
- final boolean hasZone)
- {
- this.type = type;
- this.isBefore = isBefore;
- this.isAfter = isAfter;
- this.isEqual = isEqual;
- this.now = now;
- this.withZone = withZone;
- this.hasZone = hasZone;
- }
+ public final transient boolean isDynamic;
/**
- * Returns whether the end point will be determined dynamically every time
that a method is invoked.
- *
- * @return whether the methods are determined dynamically on an
instance-by-instance basis.
- */
- public boolean isDynamic() {
- return false;
+ * Creates a new set of operators. This method is for subclasses only.
+ * For getting a {@code TimeMethods} instance, see {@link #forType(Class)}.
+ */
+ private TimeMethods(
+ final Class<T> type,
+ final Function<Object,T> converter,
+ final BiPredicate<T,T> isBefore,
+ final BiPredicate<T,T> isAfter,
+ final BiPredicate<T,T> isEqual,
+ final Supplier<T> now,
+ final BiFunction<T, ZoneId, Temporal> withZone,
+ final boolean hasZone,
+ final boolean isDynamic)
+ {
+ this.type = type;
+ this.converter = converter;
+ this.isBefore = isBefore;
+ this.isAfter = isAfter;
+ this.isEqual = isEqual;
+ this.now = now;
+ this.withZone = withZone;
+ this.hasZone = hasZone;
+ this.isDynamic = isDynamic;
}
/**
* Returns the predicate to use for this test.
+ * The expected type of the first operand is always {@code <T>}.
+ * The expected type of the second operand will be either {@code <T>} or
{@code Object},
+ * depending on the value of {@code t2}.
*
- * @param test the test to apply (before, after and/or equal).
+ * @param test the test to apply (before, after and/or equal).
+ * @param t2 expected class of the second operand.
* @return the predicate for the requested test.
*/
- public final BiPredicate<T,T> predicate(final Test test) {
- return test.predicate(this);
+ public final BiPredicate<T,?> predicate(final Test test, final Class<?>
t2) {
+ final BiPredicate<T,T> predicate = test.predicate(this);
+ if (type.isAssignableFrom(t2)) {
+ return predicate;
+ }
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
+ Function<Object, T> converter = this.converter;
+ return (T self, Object other) -> predicate.test(self,
converter.apply(other));
}
/**
- * Delegates the comparison to the method identified by the {@code test}
argument.
- * This method is overridden in subclasses where the delegation can be
more direct.
+ * Returns {@code true} if both arguments are non-null and this comparison
evaluates to {@code true}.
+ * The type of the objects being compared is determined dynamically, which
has a performance cost.
+ * The {@code TimeMethods.compare(…)} methods should be preferred when the
type is known in advance.
*
- * @param test the test to apply (before, after and/or equal).
- * @param self the object on which to invoke the method identified by
{@code test}.
- * @param other the argument to give to the test method call.
- * @return the result of performing the comparison identified by {@code
test}.
- */
- boolean delegate(final Test test, final T self, final T other) {
- return test.compare(this, self, other);
- }
-
- /**
- * Compares an object of class {@code <T>} with a temporal object of
unknown class.
- * The other object is typically the beginning or ending of a period.
+ * <p>This method is equivalent to {@link #compareIfTemporal(Test, Object,
Object)} except for the
+ * return type, which is simplified to the primitive type.</p>
*
* @param test the test to apply (before, after and/or equal).
- * @param self the object on which to invoke the method identified by
{@code test}.
- * @param other the argument to give to the test method call.
- * @return the result of performing the comparison identified by {@code
test}.
- * @throws DateTimeException if the two objects cannot be compared.
+ * @param self the object on which to invoke the method identified by
{@code test}, or {@code null} if none.
+ * @param other the argument to give to the test method call, or {@code
null} if none.
+ * @return the comparison result, or {@code false} if the given objects
were not recognized as temporal.
+ * @throws DateTimeException if the two objects are temporal objects but
cannot be compared.
*/
- @SuppressWarnings("unchecked")
- public final boolean compare(final Test test, final T self, final
TemporalAccessor other) {
- if (type.isInstance(other)) {
- return delegate(test, self, (T) other); // Safe because of
above `isInstance(…)` check.
- }
- return compareAsInstants(test, accessor(self), other);
+ private static boolean compareIfTemporalElseFalse(final Test test, final
Object self, final Object other) {
+ Boolean c = compareIfTemporal(test, self, other);
+ return (c != null) && c;
}
/**
@@ -294,7 +300,7 @@ public class TimeMethods<T> implements Serializable {
}
// Use `||` because an operand by still be a `java.utl.Date`.
if (isTemporal || self instanceof Temporal || other instanceof
Temporal) {
- return compareAny(test, self, other);
+ return compareTemporalOrDate(test, self, other);
}
return null;
}
@@ -311,15 +317,14 @@ public class TimeMethods<T> implements Serializable {
* @throws DateTimeException if the two objects cannot be compared.
*/
public static boolean compareLenient(final Test test, final Temporal self,
final Temporal other) {
- if (self != null && other != null) {
- Boolean c = compareAny(test, self, other);
- if (c != null) return c;
+ if (self == null || other == null) {
+ return false;
}
- return false;
+ return compareTemporalOrDate(test, self, other);
}
/**
- * Implementation of lenient comparisons.
+ * Implementation of lenient comparisons between non-null instances of
arbitrary temporal or date types.
* Temporal objects have complex conversion rules. We take Instant as the
most accurate and unambiguous type.
* So if at least one value is an Instant, try to unconditionally promote
the other value to an Instant too.
* This conversion will fail if the other object has some undefined
fields. For example {@link java.sql.Date}
@@ -328,12 +333,13 @@ public class TimeMethods<T> implements Serializable {
* @param test the test to apply (before, after and/or equal).
* @param self the object on which to invoke the method identified by
{@code test}.
* @param other the argument to give to the test method call.
- * @return the comparison result, or {@code null} if the given objects
were not recognized as temporal.
+ * @return the result of performing the comparison identified by {@code
test}.
* @throws DateTimeException if the two objects cannot be compared.
*/
- private static Boolean compareAny(final Test test, Object self, Object
other) {
+ @SuppressWarnings("unchecked")
+ private static boolean compareTemporalOrDate(final Test test, Object self,
Object other) {
Class<?> type = self.getClass();
-adapt: if (self != other.getClass()) {
+adapt: if (type != other.getClass()) {
Temporal converted;
/*
* OffsetTime and OffsetDateTime are final classes that do not
implement a java.time.chrono interface.
@@ -390,7 +396,7 @@ adapt: if (self != other.getClass()) {
break adapt;
}
}
- // No else, we want this as a fallback.
+ // No else, we want this fallback.
if (self instanceof ChronoLocalDate) {
converted = toLocalDate(other);
if (converted != null) {
@@ -406,7 +412,7 @@ adapt: if (self != other.getClass()) {
break adapt;
}
}
- // No else, we want this as a fallback.
+ // No else, we want this fallback.
if (self instanceof LocalTime) {
converted = toLocalTime(other);
if (converted != null) {
@@ -422,84 +428,80 @@ adapt: if (self != other.getClass()) {
break adapt;
}
}
- // No else, we want this as a fallback.
- if (self instanceof Temporal && other instanceof Temporal) {
- type = Classes.findCommonClass(self.getClass(),
other.getClass());
- } else {
- return null;
+ // No else, we want this fallback.
+ final TimeMethods<?> methods = forTypes(self.getClass(),
other.getClass(), false);
+ if (methods != null && !methods.isDynamic) {
+ assert methods.type.isInstance(self) : self;
+ return ((TimeMethods) methods).convertAndCompareObject(test,
self, other);
}
+ throw new
DateTimeException(Errors.format(Errors.Keys.CannotCompareInstanceOf_2,
self.getClass(), other.getClass()));
}
- return castAndCompare(test, type, self, other);
+ /*
+ * The implementation of `TimeMethods.before/equals/after` functions
delegate to this method in the
+ * most generic cases (the `Object` and `Temporal` types declared in
the `FOR_EXACT_TYPES` map).
+ * Therefore, we must exclude the following block when `isDynamic` is
true for avoiding infinite
+ * recursive calls.
+ */
+ final TimeMethods<?> methods = forType(type, false);
+ if (methods != null && !methods.isDynamic) {
+ assert methods.type.isInstance(self) : self;
+ assert methods.type.isInstance(other) : other;
+ return test.compare((TimeMethods) methods, self, other);
+ } else if (self instanceof Comparable<?> &&
self.getClass().isInstance(other)) {
+ // Case of `Month` and `DayOfWeek` which have no "is before" or
"is after" operations.
+ return test.fromCompareTo(((Comparable) self).compareTo(other));
+ }
+ /*
+ * If we reach this point, the two operands are of different classes
and we cannot compare them directly.
+ * Try to compare the two operands as instants on the timeline.
+ */
+ return compareAsInstants(test, accessor(self), accessor(other));
}
/**
- * Delegates to {@link #compare(int, Class, Object, Object)} after
verification of the type.
+ * Compares an object of class {@code <T>} with a temporal object of
arbitrary class.
*
* @param test the test to apply (before, after and/or equal).
- * @param type base class of the {@code self} and {@code other}
arguments.
* @param self the object on which to invoke the method identified by
{@code test}.
* @param other the argument to give to the test method call.
* @return the result of performing the comparison identified by {@code
test}.
* @throws ClassCastException if {@code self} or {@code other} is not an
instance of {@code type}.
* @throws DateTimeException if the two objects cannot be compared.
*/
- private static <T> boolean castAndCompare(Test test, Class<T> type, Object
self, Object other) {
- return compare(test, type, type.cast(self), type.cast(other));
+ @SuppressWarnings("unchecked")
+ private boolean convertAndCompareObject(final Test test, final T self,
final Object other) {
+ if (converter != null) {
+ final T converted = converter.apply(other);
+ if (converted != null) {
+ return test.compare(this, self, converted);
+ }
+ } else if (type.isInstance(other)) {
+ return test.compare(this, self, (T) other); // Safe because of
above `isInstance(…)` check.
+ } else if (other instanceof TemporalAccessor) {
+ return compareAsInstants(test, accessor(self), (TemporalAccessor)
other);
+ }
+ throw new
DateTimeException(Errors.format(Errors.Keys.CannotCompareInstanceOf_2,
self.getClass(), other.getClass()));
}
/**
- * Compares two temporal objects of unknown class. This method needs to
check for specialized implementations
- * before to delegate to {@link Comparable#compareTo(Object)}, because the
comparison methods on the timeline
- * are not always the same as {@code compareTo(…)}.
+ * Compares an object of class {@code <T>} with a temporal object of
arbitrary class.
+ * The other object is typically the beginning or ending instant of a
period and may
+ * be converted to the {@code <T>} type before comparison.
*
- * @param <T> base class of the objects to compare.
* @param test the test to apply (before, after and/or equal).
- * @param type base class of the {@code self} and {@code other}
arguments.
* @param self the object on which to invoke the method identified by
{@code test}.
* @param other the argument to give to the test method call.
* @return the result of performing the comparison identified by {@code
test}.
* @throws DateTimeException if the two objects cannot be compared.
*/
- public static <T> boolean compare(final Test test, final Class<T> type,
final T self, final T other) {
- /*
- * The following cast is not strictly true, it should be `<? extends
T>`.
- * However, because of the `isInstance(…)` check and because <T> is
used
- * only as parameter type (no collection), it is okay to use it that
way.
- */
- final TimeMethods<? super T> tc = findSpecialized(type);
- /*
- * The implementation of `TimeMethods.before/equals/after` methods
delegate to this method in the
- * most generic case. In such cases, this block must be excluded for
avoiding a never-ending loop.
- */
- if (tc != null && !tc.isDynamic()) {
- /*
- * Found one of the special cases listed in `INTERFACES` or
`FINAL_TYPE`.
- * If the other type is compatible, the comparison is executed
directly.
- */
- if (tc.type.isInstance(other)) {
- assert tc.type.isAssignableFrom(type) : tc; // Those types
are not necessarily equal.
- return test.compare(tc, self, other);
- }
- } else if (self instanceof Comparable<?> &&
self.getClass().isInstance(other)) {
- /*
- * The type of the first operand is not a special case, but the
second operand is compatible
- * for a call to the generic `compareTo(…)` method. This case does
not happen often, because
- * not many `java.time` classes have no "is before" or "is after"
operations.
- * Some examples are `Month` and `DayOfWeek`.
- */
- @SuppressWarnings("unchecked") // Safe because
verification done by `isInstance(…)`.
- final int c = ((Comparable) self).compareTo(other);
- return test.fromCompareTo(c);
- }
- /*
- * If we reach this point, the two operands are of different classes
and we cannot compare them directly.
- * Try to compare the two operands as instants on the timeline.
- */
- return compareAsInstants(test, accessor(self), accessor(other));
+ public final boolean convertAndCompare(final Test test, final T self,
final TemporalAccessor other) {
+ return convertAndCompareObject(test, self, other);
}
/**
* Returns the given object as a temporal accessor.
+ *
+ * @throws DateTimeException if the object cannot be converted.
*/
private static TemporalAccessor accessor(final Object value) {
if (value instanceof TemporalAccessor) {
@@ -533,80 +535,128 @@ adapt: if (self != other.getClass()) {
}
/**
- * Returns the set of methods that can be invoked on instances of the
given type, or {@code null} if none.
- * This method returns only one of the methods defined in {@link
#FINAL_TYPES} or {@link #INTERFACES}.
- * It shall not try to create fallbacks.
+ * Returns the set of methods that can be invoked on instances of the
given types.
+ * If the types are too generic, then this method returns a fallback which
will check
+ * for a more specific type during filter execution.
*
- * @param <T> compile-time value of the {@code type} argument.
- * @param type the type of temporal object for which to get specialized
methods.
- * @return set of specialized methods for the given object type, or {@code
null} if none.
+ * <p>It is guaranteed that {@code self} will be assignable to the {@link
#type} of the returned value.
+ * However, {@code other} will be assignable to {@link #type} only on a
best-effort basis.
+ * The {@link #convertAndCompare(Test, Object, TemporalAccessor)} method
can be used when
+ * {@code other} is not of that type.</p>
+ *
+ * @param self the type of the first operand in comparisons.
+ * @param other the type of the second operand in comparisons.
+ * @return set of comparison methods for operands of the given types, or
{@code null} if not found.
*/
- @SuppressWarnings("unchecked")
- private static <T> TimeMethods<? super T> findSpecialized(final Class<T>
type) {
- { // Block for keeping `tc` in local scope.
- TimeMethods<?> tc = FINAL_TYPES.get(type);
- if (tc != null) {
- assert tc.type == type : tc;
- return (TimeMethods<T>) tc; // Safe because of
`==` checks.
+ public static TimeMethods<?> forTypes(final Class<?> self, final Class<?>
other) {
+ return forTypes(self, other, true);
+ }
+
+ /**
+ * Returns the set of methods that can be invoked on instances of the
given types.
+ * The {@code fallback} argument control whether to create a fallback if
the types are too generic.
+ * Fallback must be disabled when this method is invoked from {@link
#compareTemporalOrDate(Test, Object, Object)},
+ * otherwise never-ending recursive calls may happen.
+ *
+ * <p>It is guaranteed that {@code self} will be assignable to the {@link
#type} of the returned value.
+ * However, {@code other} will be assignable to {@link #type} only on a
best-effort basis.</p>
+ *
+ * @param self the type of the first operand in comparisons.
+ * @param other the type of the second operand in comparisons.
+ * @param fallback whether fallback is allowed.
+ * @return set of comparison methods for operands of the given types, or
{@code null} if not found.
+ */
+ private static TimeMethods<?> forTypes(final Class<?> self, final Class<?>
other, final boolean fallback) {
+ if (self.isAssignableFrom(other)) return forType(self, fallback);
+ if (other.isAssignableFrom(self)) return forType(other, fallback);
+ for (final Class<?> type : Classes.findCommonInterfaces(self, other)) {
+ final TimeMethods<?> methods = forType(type, false); // Fallback
not wanted here.
+ if (methods != null) {
+ return methods;
}
}
- for (TimeMethods<?> tc : INTERFACES) {
- if (tc.type.isAssignableFrom(type)) {
- return (TimeMethods<? super T>) tc; // Safe because of
`isAssignableFrom(…)` checks.
- }
+ Class<?> type = Classes.findCommonClass(self, other);
+ if (type == Object.class) {
+ type = self.getClass(); // See method javadoc.
}
- return null;
+ return forType(type, fallback);
}
/**
- * Returns the set of methods that can be invoked on instances of the
given type.
+ * Returns the set of methods that can be invoked on instances of the
given type, or {@code null} if none.
+ * If {@code fallback} is {@code false}, then this method returns only one
of the methods defined in
+ * {@link #FOR_EXACT_TYPES} or {@link #FOR_PARENT_TYPES} without trying to
create fallbacks.
*
- * @param <T> compile-time value of the {@code type} argument.
- * @param type the type of temporal object for which to get specialized
methods.
- * @return set of comparison methods for the given object type.
+ * @param <T> compile-time value of the {@code type} argument.
+ * @param type the type of temporal object for which to get
specialized methods.
+ * @param fallback whether to allow the creation of fallbacks.
+ * @return set of specialized methods for the given object type, or {@code
null} if none.
*/
- @SuppressWarnings("unchecked") // For (Comparable) casts.
- public static <T> TimeMethods<? super T> find(final Class<T> type) {
- final TimeMethods<? super T> tc = findSpecialized(type);
- if (tc != null) {
- return tc;
+ @SuppressWarnings("unchecked")
+ private static <T> TimeMethods<? super T> forType(final Class<T> type,
final boolean fallback) {
+ { // Block for keeping `methods` in local scope.
+ TimeMethods<?> methods = FOR_EXACT_TYPES.get(type);
+ if (methods != null) {
+ assert methods.type == type : methods;
+ return (TimeMethods<T>) methods; // Safe because
of `==` checks.
+ }
}
- if (Modifier.isFinal(type.getModifiers())) {
+ for (TimeMethods<?> methods : FOR_PARENT_TYPES) {
+ if (methods.type.isAssignableFrom(type)) {
+ return (TimeMethods<? super T>) methods; // Safe because
of `isAssignableFrom(…)` checks.
+ }
+ }
+ if (fallback) {
+ if (!Modifier.isFinal(type.getModifiers())) {
+ return fallback(type);
+ }
if (Comparable.class.isAssignableFrom(type)) {
- return new TimeMethods<>(type,
- (self, other) -> ((Comparable) self).compareTo(other)
< 0,
- (self, other) -> ((Comparable) self).compareTo(other)
> 0,
- (self, other) -> ((Comparable) self).compareTo(other)
== 0,
- null, null, false);
- } else {
- throw new
DateTimeException(Errors.format(Errors.Keys.CannotCompareInstanceOf_2, type,
type));
+ return new TimeMethods<>(type, null,
+ (self, other) -> ((Comparable<T>)
self).compareTo(other) < 0,
+ (self, other) -> ((Comparable<T>)
self).compareTo(other) > 0,
+ (self, other) -> ((Comparable<T>)
self).compareTo(other) == 0,
+ null, null, false, false);
}
- } else {
- return fallback(type);
}
+ return null;
}
/**
* Returns the last-resort fallback when the type of temporal objects
cannot be determined in advance.
+ * All methods delegate (indirectly) to {@link #compareIfTemporal(Test,
Object, Object)}, which will
+ * check the type at runtime.
*
* @param <T> compile-time value of the {@code type} argument.
* @param type the type of temporal object for which to get the
last-resource fallback methods.
* @return set of last-resort comparison methods for the given object type.
*/
private static <T> TimeMethods<? super T> fallback(final Class<T> type) {
- return new TimeMethods<>(type,
- (self, other) -> compare(Test.BEFORE, type, self, other),
- (self, other) -> compare(Test.AFTER, type, self, other),
- (self, other) -> compare(Test.EQUAL, type, self, other),
- null, null, false)
- {
- @Override public boolean isDynamic() {
- return true;
- }
- @Override boolean delegate(final Test test, final T self, final T
other) {
- return compare(test, type, self, other);
- }
- };
+ return new TimeMethods<>(type, null,
+ (self, other) -> compareIfTemporalElseFalse(Test.BEFORE, self,
other),
+ (self, other) -> compareIfTemporalElseFalse(Test.AFTER, self,
other),
+ (self, other) -> compareIfTemporalElseFalse(Test.EQUAL, self,
other),
+ null, null, false, true);
+ }
+
+ /**
+ * Returns the set of methods that can be invoked on instances of the
given type.
+ * If the type is too generic, then this method returns a fallback which
will check
+ * for a more specific type during filter execution.
+ *
+ * <p>The {@links Test tests} (is before, is after, is equal, <i>etc.</i>)
expect
+ * the first operand to be of type {@code <T>}. However, the second
operand may be of
+ * a different type if the {@link #convertAndCompare(Test, Object,
TemporalAccessor)}
+ * method is used.</p>
+ *
+ * @param <T> compile-time value of the {@code type} argument.
+ * @param type the type of temporal object for which to get specialized
methods.
+ * @return set of comparison methods for the given object type.
+ * @throws DateTimeException if it is known in advance that comparisons
will not be possible.
+ */
+ public static <T> TimeMethods<? super T> forType(final Class<T> type) {
+ final TimeMethods<? super T> methods = forType(type, true);
+ if (methods != null) return methods;
+ throw new
DateTimeException(Errors.format(Errors.Keys.CannotCompareInstanceOf_2, type,
type));
}
/**
@@ -617,7 +667,7 @@ adapt: if (self != other.getClass()) {
* @throws ObjectStreamException if the serialized object contains invalid
data.
*/
private Object readResolve() throws ObjectStreamException {
- return find(type);
+ return forType(type);
}
/**
@@ -663,10 +713,10 @@ adapt: if (self != other.getClass()) {
* @param allowAdd whether to allow the addition of a time zone in an
object that initially had none.
* @return a temporal object with the specified timezone, if it was
possible to apply a timezone.
*/
- public static <T> Optional<Temporal> withZone(final T time, final ZoneId
timezone, final boolean allowAdd) {
+ public static <T extends Temporal> Optional<Temporal> withZone(final T
time, final ZoneId timezone, final boolean allowAdd) {
if (time != null) {
- final TimeMethods<? super T> methods =
find(Classes.getClass(time));
- if ((methods.hasZone | allowAdd) && methods.withZone != null) {
+ final TimeMethods<? super T> methods =
forType(Classes.getClass(time), false);
+ if (methods != null && (methods.hasZone | allowAdd) &&
methods.withZone != null) {
return Optional.ofNullable(methods.withZone.apply(time,
timezone));
}
}
@@ -678,11 +728,11 @@ adapt: if (self != other.getClass()) {
* Those types need to be checked with {@link
Class#isAssignableFrom(Class)} in iteration order.
*/
@SuppressWarnings({"rawtypes", "unchecked"}) // For `Chrono*`
interfaces, because they are parameterized.
- private static final TimeMethods<?>[] INTERFACES = {
- new TimeMethods<>(ChronoZonedDateTime.class,
ChronoZonedDateTime::isBefore, ChronoZonedDateTime::isAfter,
ChronoZonedDateTime::isEqual, ZonedDateTime::now,
ChronoZonedDateTime::withZoneSameInstant, true),
- new TimeMethods<>(ChronoLocalDateTime.class,
ChronoLocalDateTime::isBefore, ChronoLocalDateTime::isAfter,
ChronoLocalDateTime::isEqual, LocalDateTime::now, ChronoLocalDateTime::atZone,
false),
- new TimeMethods<>( ChronoLocalDate.class,
ChronoLocalDate::isBefore, ChronoLocalDate::isAfter,
ChronoLocalDate::isEqual, LocalDate::now, null, false),
- new TimeMethods<>( Date.class, Date::
before, Date:: after, Date::equals,
Date::new, TimeMethods::atZone, true)
+ private static final TimeMethods<?>[] FOR_PARENT_TYPES = {
+ new TimeMethods<>(ChronoZonedDateTime.class,
null, ChronoZonedDateTime::isBefore, ChronoZonedDateTime::isAfter,
ChronoZonedDateTime::isEqual, ZonedDateTime::now,
ChronoZonedDateTime::withZoneSameInstant, true, false),
+ new TimeMethods<>(ChronoLocalDateTime.class,
TimeMethods::toLocalDateTime, ChronoLocalDateTime::isBefore,
ChronoLocalDateTime::isAfter, ChronoLocalDateTime::isEqual, LocalDateTime::now,
ChronoLocalDateTime::atZone, false, false),
+ new TimeMethods<>( ChronoLocalDate.class,
TimeMethods::toLocalDate, ChronoLocalDate::isBefore,
ChronoLocalDate::isAfter, ChronoLocalDate::isEqual, LocalDate::now,
null, false, false),
+ new TimeMethods<>( Date.class,
null, Date:: before, Date:: after,
Date::equals, Date::new, TimeMethods::atZone, true, false)
};
/*
@@ -694,7 +744,7 @@ adapt: if (self != other.getClass()) {
* Operators for all supported temporal types for which there is no need
to check for subclasses.
* Those classes should be final because they are compared by equality
instead of "instance of".
* The two last entries are not final, but we really want to ignore all
their subtypes.
- * All those types should be tested before {@link #INTERFACES} because
this check is quick.
+ * All those types should be tested before {@link #FOR_PARENT_TYPES}
because this check is quick.
*
* <h4>Implementation note</h4>
* {@link Year}, {@link YearMonth}, {@link MonthDay}, {@link LocalTime}
and {@link Instant}
@@ -702,22 +752,22 @@ adapt: if (self != other.getClass()) {
* case the implementations change in the future, and also for performance
reason, because
* the code working on generic {@link Comparable} needs to check for
special cases again.
*/
- private static final Map<Class<?>, TimeMethods<?>> FINAL_TYPES =
Map.ofEntries(
- entry(new TimeMethods<>(OffsetDateTime.class,
OffsetDateTime::isBefore, OffsetDateTime::isAfter, OffsetDateTime::isEqual,
OffsetDateTime::now, TimeMethods::withZoneSameInstant, true)),
- entry(new TimeMethods<>( ZonedDateTime.class,
ZonedDateTime::isBefore, ZonedDateTime::isAfter, ZonedDateTime::isEqual,
ZonedDateTime::now, ZonedDateTime::withZoneSameInstant, true)),
- entry(new TimeMethods<>( LocalDateTime.class,
LocalDateTime::isBefore, LocalDateTime::isAfter, LocalDateTime::isEqual,
LocalDateTime::now, LocalDateTime::atZone, false)),
- entry(new TimeMethods<>( LocalDate.class,
LocalDate::isBefore, LocalDate::isAfter, LocalDate::isEqual,
LocalDate::now, null, false)),
- entry(new TimeMethods<>( OffsetTime.class,
OffsetTime::isBefore, OffsetTime::isAfter, OffsetTime::isEqual,
OffsetTime::now, TimeMethods::withOffsetSameInstant, true)),
- entry(new TimeMethods<>( LocalTime.class,
LocalTime::isBefore, LocalTime::isAfter, LocalTime::equals,
LocalTime::now, TimeMethods::atOffset, false)),
- entry(new TimeMethods<>( Year.class,
Year::isBefore, Year::isAfter, Year::equals,
Year::now, null, false)),
- entry(new TimeMethods<>( YearMonth.class,
YearMonth::isBefore, YearMonth::isAfter, YearMonth::equals,
YearMonth::now, null, false)),
- entry(new TimeMethods<>( MonthDay.class,
MonthDay::isBefore, MonthDay::isAfter, MonthDay::equals,
MonthDay::now, null, false)),
- entry(new TimeMethods<>( Instant.class,
Instant::isBefore, Instant::isAfter, Instant::equals,
Instant::now, Instant::atZone, true)),
+ private static final Map<Class<?>, TimeMethods<?>> FOR_EXACT_TYPES =
Map.ofEntries(
+ entry(new TimeMethods<>(OffsetDateTime.class,
TimeMethods::toOffsetDateTime, OffsetDateTime::isBefore,
OffsetDateTime::isAfter, OffsetDateTime::isEqual, OffsetDateTime::now,
TimeMethods::withZoneSameInstant, true, false)),
+ entry(new TimeMethods<>( ZonedDateTime.class,
null, ZonedDateTime::isBefore, ZonedDateTime::isAfter,
ZonedDateTime::isEqual, ZonedDateTime::now,
ZonedDateTime::withZoneSameInstant, true, false)),
+ entry(new TimeMethods<>( LocalDateTime.class,
null, LocalDateTime::isBefore, LocalDateTime::isAfter,
LocalDateTime::isEqual, LocalDateTime::now, LocalDateTime::atZone, false,
false)),
+ entry(new TimeMethods<>( LocalDate.class,
null, LocalDate::isBefore, LocalDate::isAfter,
LocalDate::isEqual, LocalDate::now, null, false, false)),
+ entry(new TimeMethods<>( OffsetTime.class,
null, OffsetTime::isBefore, OffsetTime::isAfter,
OffsetTime::isEqual, OffsetTime::now, TimeMethods::withOffsetSameInstant,
true, false)),
+ entry(new TimeMethods<>( LocalTime.class,
TimeMethods::toLocalTime, LocalTime::isBefore, LocalTime::isAfter,
LocalTime::equals, LocalTime::now, TimeMethods::atOffset, false,
false)),
+ entry(new TimeMethods<>( Year.class,
null, Year::isBefore, Year::isAfter,
Year::equals, Year::now, null, false, false)),
+ entry(new TimeMethods<>( YearMonth.class,
null, YearMonth::isBefore, YearMonth::isAfter,
YearMonth::equals, YearMonth::now, null, false, false)),
+ entry(new TimeMethods<>( MonthDay.class,
null, MonthDay::isBefore, MonthDay::isAfter,
MonthDay::equals, MonthDay::now, null, false, false)),
+ entry(new TimeMethods<>( Instant.class,
TimeMethods::toInstant, Instant::isBefore, Instant::isAfter,
Instant::equals, Instant::now, Instant::atZone, true, false)),
entry(fallback(Temporal.class)), // Frequently declared type.
Intentionally no "instance of" checks.
entry(fallback(Object.class))); // Not a final class, but to be
used when the declared type is Object.
/**
- * Helper method for adding entries to the {@link #FINAL_TYPES} map.
+ * Helper method for adding entries to the {@link #FOR_EXACT_TYPES} map.
* Shall be used only for final classes.
*/
private static Map.Entry<Class<?>, TimeMethods<?>> entry(final
TimeMethods<?> op) {
diff --git
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
index 01afe9bf99..cdb9fbe5ea 100644
---
a/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
+++
b/endorsed/src/org.apache.sis.storage/test/org/apache/sis/storage/FeatureQueryTest.java
@@ -22,6 +22,7 @@ import java.util.Set;
import java.util.HashSet;
import java.util.Iterator;
import java.util.stream.Collectors;
+import java.time.LocalDate;
import java.awt.geom.Point2D;
import org.opengis.metadata.acquisition.GeometryType;
import org.apache.sis.feature.Features;
@@ -96,7 +97,7 @@ public final class FeatureQueryTest extends TestCase {
* Creates a simple feature with a property flagged as an identifier.
*/
private void createFeatureWithIdentifier() {
- final FeatureTypeBuilder ftb = new
FeatureTypeBuilder().setName("Test");
+ final var ftb = new FeatureTypeBuilder().setName("Test");
ftb.addAttribute(String.class).setName("id").addRole(AttributeRole.IDENTIFIER_COMPONENT);
final FeatureType type = ftb.build();
features = new Feature[] {
@@ -115,7 +116,7 @@ public final class FeatureQueryTest extends TestCase {
* @return the points created by this method in no particular order.
*/
private Set<Point2D.Double> createFeaturesWithGeometry(final
GeometryLibrary library) {
- final FeatureTypeBuilder ftb = new FeatureTypeBuilder(null, library,
null).setName("Test");
+ final var ftb = new FeatureTypeBuilder(null, library,
null).setName("Test");
ftb.addAttribute(GeometryType.POINT).setCRS(HardCodedCRS.WGS84_LATITUDE_FIRST).setName("point");
final FeatureType type = ftb.build();
final var points = new HashSet<Point2D.Double>();
@@ -128,7 +129,7 @@ public final class FeatureQueryTest extends TestCase {
final Feature f = type.newInstance();
f.setPropertyValue("point", factory.createPoint(point.x, point.y));
features[i] = f;
- };
+ }
this.features = features;
featureSet = new MemoryFeatureSet(null, type, Arrays.asList(features));
return points;
@@ -290,6 +291,40 @@ public final class FeatureQueryTest extends TestCase {
verifyQueryResult(3);
}
+ /**
+ * Verifies the effect of {@link FeatureQuery#setSelection(Filter)} on a
property having a date.
+ *
+ * @throws DataStoreException if an error occurred while executing the
query.
+ */
+ @Test
+ public void testSelectionOfDate() throws DataStoreException {
+ // Prepare the feature instances.
+ {
+ final var ftb = new FeatureTypeBuilder().setName("Test");
+ ftb.addAttribute(LocalDate.class).setName("value1");
+ final FeatureType type = ftb.build();
+ features = new Feature[4];
+ Arrays.setAll(features, (i) -> {
+ Feature feature = type.newInstance();
+ feature.setPropertyValue("value1", LocalDate.of(2000, 1, 10 +
i));
+ return feature;
+ });
+ featureSet = new MemoryFeatureSet(null, type,
Arrays.asList(features));
+ }
+ // Prepare the query.
+ {
+ final FilterFactory<Feature,?,?> ff =
DefaultFilterFactory.forFeatures();
+ query.setSelection(ff.lessOrEqual(
+ ff.property("value1", LocalDate.class),
+ ff.literal(LocalDate.of(2000, 1, 12))));
+ assertXPathsEqual("value1");
+ }
+ // Verify the result.
+ final FeatureSet fs = query.execute(featureSet);
+ final Feature[] result = fs.features(false).toArray(Feature[]::new);
+ assertArrayEquals(Arrays.copyOf(features, 3), result);
+ }
+
/**
* Verifies the effect of {@link
FeatureQuery#setProjection(FeatureQuery.Column[])}.
*