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 9f766a14c9 Consolidation of the conversion of temporal objects before
comparison. It fixes a `NullPointerException` when there is no converter
available.
9f766a14c9 is described below
commit 9f766a14c9c4365cdd27e6516e98a3e8e9532214
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Jan 26 13:07:30 2026 +0100
Consolidation of the conversion of temporal objects before comparison.
It fixes a `NullPointerException` when there is no converter available.
---
.../org/apache/sis/filter/ComparisonFilter.java | 51 ++++----
.../main/org/apache/sis/temporal/TimeMethods.java | 131 +++++++++++++++------
.../main/org/apache/sis/converter/ClassPair.java | 2 +-
.../apache/sis/converter/ConverterRegistry.java | 2 +-
.../org/apache/sis/converter/DateConverter.java | 14 ++-
.../org/apache/sis/converter/StringConverter.java | 1 +
.../org/apache/sis/converter/SystemRegistry.java | 2 +-
.../main/org/apache/sis/system/Loggers.java | 3 +-
8 files changed, 141 insertions(+), 65 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 471fd683fd..db3a7cf787 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,8 +19,6 @@ 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;
@@ -170,15 +168,17 @@ abstract class ComparisonFilter<R> extends
BinaryFunctionWidening<R, Object, Obj
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));
+ final Class<?> t1, t2;
+ if (isSpecialized(t1 = getResultClass(expression1)) &&
+ isSpecialized(t2 = getResultClass(expression2)))
+ {
+ final var numeric = optimized.new Numeric();
+ if (numeric.evaluator != null) {
+ return numeric;
+ }
+ final var temporal = new Time<>(TimeMethods.forTypes(t1, t2),
t2);
+ if (temporal.evaluator != null) {
+ return temporal;
}
}
}
@@ -186,12 +186,12 @@ abstract class ComparisonFilter<R> extends
BinaryFunctionWidening<R, Object, Obj
}
/**
- * 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}.
+ * Returns whether the given type is non-null and something more
specialized than {@code Object}.
+ * This is used for avoiding unnecessary class-loading of {@link Numeric}
and {@link Time} when
+ * they are sure to be unsuccessful.
*/
- private static boolean isTemporal(final Class<?> type) {
- return (type != null) && (Temporal.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type));
+ private static boolean isSpecialized(final Class<?> type) {
+ return (type != null) && (type != Object.class);
}
/**
@@ -222,15 +222,17 @@ abstract class ComparisonFilter<R> extends
BinaryFunctionWidening<R, Object, Obj
/**
* An optimized versions of this filter for the case where the operands
are temporal.
*/
- private final class Time extends Node implements Filter<R> {
+ private final class Time<T,S> 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;
+ @SuppressWarnings("serial") final BiPredicate<T,S> evaluator;
- /** Creates a new filter. */
- Time(final BiPredicate<?,?> evaluator) {this.evaluator = evaluator;}
+ /** Creates a new filter. Callers must verifies that {@link
#evaluator} is non-null. */
+ Time(final TimeMethods<T> methods, final Class<S> otherType) {
+ evaluator = (methods != null) ? methods.predicate(temporalTest(),
otherType) : null;
+ }
/** Delegates to the enclosing class.*/
@Override public CodeList<?> getOperatorType() {return
ComparisonFilter.this.getOperatorType();}
@@ -239,13 +241,14 @@ abstract class ComparisonFilter<R> extends
BinaryFunctionWidening<R, Object, Obj
@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);
+ @SuppressWarnings("unchecked")
+ final T left = (T) expression1.apply(candidate);
if (left != null) {
- final Object right = expression2.apply(candidate);
+ @SuppressWarnings("unchecked")
+ final S right = (S) expression2.apply(candidate);
if (right != null) {
- return ((BiPredicate) evaluator).test(left, right);
+ return evaluator.test(left, right);
}
}
return false;
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 03fde3581c..5bbf882899 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
@@ -20,6 +20,7 @@ import java.util.Map;
import java.util.Date;
import java.util.Calendar;
import java.util.Optional;
+import java.util.logging.Logger;
import java.util.function.Supplier;
import java.util.function.Function;
import java.util.function.BiFunction;
@@ -47,7 +48,10 @@ import java.lang.reflect.Modifier;
import java.io.Serializable;
import java.io.ObjectStreamException;
import org.apache.sis.util.Classes;
+import org.apache.sis.util.ObjectConverters;
+import org.apache.sis.util.UnconvertibleObjectException;
import org.apache.sis.util.internal.shared.Strings;
+import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Errors;
@@ -70,6 +74,11 @@ import org.apache.sis.util.resources.Errors;
* @author Martin Desruisseaux (Geomatys)
*/
public final class TimeMethods<T> implements Serializable {
+ /**
+ * Logger for warnings.
+ */
+ private static final Logger LOGGER =
Logger.getLogger("org.apache.sis.temporal");
+
/**
* The test to apply: equal, before or after.
*
@@ -160,8 +169,21 @@ public final class TimeMethods<T> implements Serializable {
/**
* 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.
+ *
+ * <p><b>Design note:</b> the purpose of this field overlaps with {@link
org.apache.sis.util.ObjectConverters}.
+ * Those converters are not yet registered as {@link
org.apache.sis.util.ObjectConverter}s because they expect
+ * {@code Object} in argument (which is too generic for {@code
ObjectConverter}) for performance reasons,
+ * conversions may lost information, and we may report these lost with
{@link #ignoringField(ChronoField)}
+ * in the future. Another difference is that the function returns {@code
null} instead of throwing an exception
+ * if an operand is not recognized.</p>
+ *
+ * @see #toOffsetDateTime(Object)
+ * @see #toLocalDateTime(Object)
+ * @see #toLocalDate(Object)
+ * @see #toLocalTime(Object)
+ * @see #toInstant(Object)
*/
- public final transient Function<Object, T> converter;
+ private final transient Function<Object, T> converter;
/**
* Predicate to execute for testing the ordering between temporal objects.
@@ -176,7 +198,7 @@ public final class TimeMethods<T> implements Serializable {
*
* @see #now()
*/
- public final transient Supplier<T> now;
+ private final transient Supplier<T> now;
/**
* Function to execute for getting another temporal object with the given
timezone, or {@code null} if none.
@@ -188,7 +210,7 @@ public final class TimeMethods<T> implements Serializable {
*
* @see #withZone(Object, ZoneId, boolean)
*/
- public final transient BiFunction<T, ZoneId, Temporal> withZone;
+ private final transient BiFunction<T, ZoneId, Temporal> withZone;
/**
* Whether the temporal object have a time zone, explicitly or implicitly.
@@ -226,24 +248,67 @@ public final class TimeMethods<T> implements Serializable
{
this.isDynamic = isDynamic;
}
+ /**
+ * Returns a converter from {@code otherType} to {@link #type},
+ * or {@code null} if there is no need for converter.
+ *
+ * @param <S> compile-time value of {@code otherType}.
+ * @param otherType the type of values expected as input by the
converter.
+ * @return converter from {@code otherType} to {@link #type}, or {@code
null}.
+ */
+ @SuppressWarnings("unchecked")
+ private <S> Function<? super S, T> converter(final Class<S> otherType) {
+ if (type.isAssignableFrom(otherType)) {
+ return null;
+ }
+ /*
+ * Check for more specific conversions before to fallback on
`converter` because the most specific
+ * cases are likely to be more efficient. We must exclude the
`java.util.Date` type because of the
+ * complication of the `java.sql.Date` and `java.sql.Time` subtypes.
+ */
+ if (otherType != Object.class &&
!Date.class.isAssignableFrom(otherType)) try {
+ final Function<? super S, ? extends T> castOrConvert =
ObjectConverters.find(otherType, type);
+ return (S other) -> {
+ try {
+ return castOrConvert.apply(other);
+ } catch (UnconvertibleObjectException e) {
+ Logging.ignorableException(LOGGER, TimeMethods.class,
"converter", e);
+ return null;
+ }
+ };
+ } catch (UnconvertibleObjectException e) {
+ Logging.ignorableException(LOGGER, TimeMethods.class, "converter",
e);
+ }
+ if (converter != null) {
+ return converter;
+ }
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
+ final Class<T> type = this.type;
+ return (S other) -> type.isInstance(other) ? (T) other : null;
+ }
+
/**
* 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 t2 expected class of the second operand.
+ * @param <S> compile-time value of {@code otherType}.
+ * @param test the test to apply (before, after and/or equal).
+ * @param otherType expected class of the second operand.
* @return the predicate for the requested test.
*/
- public final BiPredicate<T,?> predicate(final Test test, final Class<?>
t2) {
+ @SuppressWarnings("unchecked")
+ public final <S> BiPredicate<T,S> predicate(final Test test, final
Class<S> otherType) {
final BiPredicate<T,T> predicate = test.predicate(this);
- if (type.isAssignableFrom(t2)) {
- return predicate;
+ final Function<? super S, T> castOrConvert = converter(otherType);
+ if (castOrConvert == null) {
+ return (BiPredicate) predicate;
}
- @SuppressWarnings("LocalVariableHidesMemberVariable")
- Function<Object, T> converter = this.converter;
- return (T self, Object other) -> predicate.test(self,
converter.apply(other));
+ return (T self, S other) -> {
+ final T converted = castOrConvert.apply(other);
+ return (converted != null) && predicate.test(self, converted);
+ };
}
/**
@@ -305,6 +370,14 @@ public final class TimeMethods<T> implements Serializable {
return null;
}
+ /**
+ * Returns whether the given type is considered temporal. This is
necessary for avoiding that
+ * comparable objects such as {@link String} are wrongly handled by {@code
TimeMethods}.
+ */
+ private static boolean isTemporal(final Class<?> type) {
+ return (type != null) && (Temporal.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type));
+ }
+
/**
* Returns {@code true} if both arguments are non-null and the specified
comparison evaluates to {@code true}.
* The type of the objects being compared is determined dynamically, which
has a performance cost.
@@ -432,7 +505,7 @@ adapt: if (type != other.getClass()) {
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);
+ return ((TimeMethods) methods).convertAndCompare(test, self,
other);
}
throw new
DateTimeException(Errors.format(Errors.Keys.CannotCompareInstanceOf_2,
self.getClass(), other.getClass()));
}
@@ -460,16 +533,20 @@ adapt: if (type != other.getClass()) {
/**
* 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.
+ *
+ * <p>The type of the {@code other} argument should be {@link
TemporalAccessor},
+ * but the method signature uses {@code Object} for accepting also {@link
Date}.</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.
+ * @param other the argument to give to the test method call after
conversion.
* @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.
*/
@SuppressWarnings("unchecked")
- private boolean convertAndCompareObject(final Test test, final T self,
final Object other) {
+ public boolean convertAndCompare(final Test test, final T self, final
Object other) {
if (converter != null) {
final T converted = converter.apply(other);
if (converted != null) {
@@ -483,21 +560,6 @@ adapt: if (type != other.getClass()) {
throw new
DateTimeException(Errors.format(Errors.Keys.CannotCompareInstanceOf_2,
self.getClass(), other.getClass()));
}
- /**
- * 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 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.
- */
- 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.
*
@@ -549,7 +611,10 @@ adapt: if (type != other.getClass()) {
* @return set of comparison methods for operands of the given types, or
{@code null} if not found.
*/
public static TimeMethods<?> forTypes(final Class<?> self, final Class<?>
other) {
- return forTypes(self, other, true);
+ if (isTemporal(self) && isTemporal(other)) {
+ return forTypes(self, other, true);
+ }
+ return null;
}
/**
@@ -857,7 +922,7 @@ adapt: if (type != other.getClass()) {
} catch (UnsupportedOperationException e) {
/*
* java.sql.Date and java.sql.Time cannot be converted to
Instant because a part
- * of their coordinates on the timeline is undefined. For
example in the case of
+ * of their coordinates on the timeline is undefined. For
example, in the case of
* java.sql.Date the hours, minutes and seconds are
unspecified (which is not the
* same thing as assuming that those values are zero).
*/
@@ -950,7 +1015,7 @@ adapt: if (type != other.getClass()) {
*
* <ul>
* <li>{@link ChronoField#OFFSET_SECONDS}: time zone is ignored.</li>
- * <li>{@link ChronoField#SECOND_OF_DAY}: time of dat and time zone are
ignored.</li>
+ * <li>{@link ChronoField#SECOND_OF_DAY}: time of {@code DateTime} and
time zone are ignored.</li>
* </ul>
*
* @param field the field which is ignored.
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ClassPair.java
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ClassPair.java
index ecaab798df..814b30a143 100644
---
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ClassPair.java
+++
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ClassPair.java
@@ -136,7 +136,7 @@ class ClassPair<S,T> implements Serializable {
@Override
public boolean equals(final Object other) {
if (other instanceof ClassPair<?,?>) {
- final ClassPair<?,?> that = (ClassPair<?,?>) other;
+ final var that = (ClassPair<?,?>) other;
return sourceClass == that.sourceClass &&
targetClass == that.targetClass;
}
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ConverterRegistry.java
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ConverterRegistry.java
index e59c2562b9..5e9b3694f4 100644
---
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ConverterRegistry.java
+++
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/ConverterRegistry.java
@@ -442,7 +442,7 @@ public class ConverterRegistry {
public <S,T> ObjectConverter<? super S, ? extends T> find(final Class<S>
sourceClass, final Class<T> targetClass)
throws UnconvertibleObjectException
{
- final ClassPair<S,T> key = new ClassPair<>(sourceClass, targetClass);
+ final var key = new ClassPair<S,T>(sourceClass, targetClass);
synchronized (converters) {
ObjectConverter<? super S, ? extends T> converter = get(key);
if (converter != null) {
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/DateConverter.java
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/DateConverter.java
index 966386cf04..70eeac72a2 100644
---
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/DateConverter.java
+++
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/DateConverter.java
@@ -27,7 +27,15 @@ import org.apache.sis.math.FunctionProperty;
/**
* Handles conversions from {@link Date} to various objects.
* Note that there is no converter between {@link String} and {@link
java.util.Date}.
- * The {@link java.time.Instant} class should be used instead.
+ * For conversions from {@code String}, use the {@link java.time.Instant}
class instead.
+ *
+ * <h2>Unconvertible dates</h2>
+ * While {@link java.util.Date} is generally convertible to {@link
java.time.Instant},
+ * the {@link java.sql.Date} and {@link java.sql.Time} subclasses are not
convertible:
+ * e.g. {@link java.sql.Date#toInstant()} throws {@link
UnsupportedOperationException}.
+ * We could handle this special case in {@link SystemRegistry} for detecting
early that
+ * a conversion will never succeed, but it does not seem worth to add this
complexity
+ * for now, especially since users could override the above-cited method.
*
* <h2>Immutability and thread safety</h2>
* This base class and all inner classes are immutable, and thus inherently
thread-safe.
@@ -129,7 +137,7 @@ abstract class DateConverter<T> extends
SystemConverter<Date,T> {
}
/**
- * From {@code Date} to SQL {@code Date}.
+ * From {@code Date} to <abbr>SQL</abbr> {@code Date}.
* The inverse of this converter is the identity conversion.
*/
public static final class SQL extends DateConverter<java.sql.Date> {
@@ -149,7 +157,7 @@ abstract class DateConverter<T> extends
SystemConverter<Date,T> {
}
/**
- * From {@code Date} to SQL {@code Timestamp}.
+ * From {@code Date} to <abbr>SQL</abbr> {@code Timestamp}.
* The inverse of this converter is the identity conversion.
*/
public static final class Timestamp extends
DateConverter<java.sql.Timestamp> {
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/StringConverter.java
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/StringConverter.java
index 1cfd70527f..9981e939a8 100644
---
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/StringConverter.java
+++
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/StringConverter.java
@@ -85,6 +85,7 @@ abstract class StringConverter<T> extends
SystemConverter<String, T> {
*
* @param targetClass the {@linkplain #getTargetClass() target class}.
*/
+ @SuppressWarnings("OverridableMethodCallInConstructor")
StringConverter(final Class<T> targetClass) {
super(String.class, targetClass);
inverse = createInverse();
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/SystemRegistry.java
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/SystemRegistry.java
index b6f9c47eb3..9ccec8df9e 100644
---
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/SystemRegistry.java
+++
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/converter/SystemRegistry.java
@@ -28,7 +28,7 @@ import org.apache.sis.system.Modules;
/**
- * The Apache SIS system-wide {@link ConverterRegistry}.
+ * The Apache <abbr>SIS</abbr> system-wide {@link ConverterRegistry}.
* This class serves two purposes:
*
* <ul>
diff --git
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/Loggers.java
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/Loggers.java
index a5b6928ddf..e330fb4ef5 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/Loggers.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/system/Loggers.java
@@ -42,8 +42,7 @@ public final class Loggers {
public static final String ROOT = "org.apache.sis";
/**
- * The logger for Apache SIS internal operations. The name of this logger
does not match the package name
- * of the classes using it, because this logger name does not have the
{@code "internal"} part in it.
+ * The logger for Apache SIS internal operations.
*/
public static final String SYSTEM = "org.apache.sis.system";