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 c1c5e12061 Allow the association of GML identifiers to temporal 
object. It requires making temporal objects lenient comparable.
c1c5e12061 is described below

commit c1c5e1206100ba87fd080ca3643627170c028d93
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Jun 14 14:33:02 2024 +0200

    Allow the association of GML identifiers to temporal object.
    It requires making temporal objects lenient comparable.
---
 .../org/apache/sis/filter/TemporalOperation.java   | 184 ++++++++++++---------
 .../org/apache/sis/temporal/DefaultInstant.java    |  39 ++++-
 .../org/apache/sis/temporal/DefaultPeriod.java     |  34 +++-
 .../org/apache/sis/temporal/GeneralDuration.java   |  21 ++-
 .../org/apache/sis/temporal/TemporalObject.java    |  89 ++++++++++
 .../main/org/apache/sis/temporal/TimeMethods.java  |  20 ++-
 .../apache/sis/xml/bind/IdentifierMapAdapter.java  |   1 +
 .../sis/metadata/iso/extent/DefaultExtentTest.java |   3 +-
 8 files changed, 297 insertions(+), 94 deletions(-)

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 1e6ae3ed5d..a59b174dc6 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
@@ -16,10 +16,10 @@
  */
 package org.apache.sis.filter;
 
+import java.util.Optional;
 import java.io.Serializable;
 import java.time.DateTimeException;
 import java.time.temporal.Temporal;
-import org.apache.sis.util.Classes;
 import org.apache.sis.util.privy.Strings;
 import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.temporal.TimeMethods;
@@ -29,7 +29,9 @@ import static org.apache.sis.temporal.TimeMethods.EQUAL;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.temporal.Period;
+import org.opengis.temporal.Instant;
 import org.opengis.filter.TemporalOperatorName;
+import org.opengis.temporal.IndeterminateValue;
 
 
 /**
@@ -174,32 +176,6 @@ abstract class TemporalOperation<T> implements 
Serializable {
      */
     protected abstract boolean evaluate(Period self, Period other);
 
-    /**
-     * Returns the beginning of the given period, or {@code null} if 
indeterminate.
-     *
-     * @param  p the period from which to get the beginning.
-     * @return beginning of the given period, or {@code null} if indeterminate.
-     *
-     * @todo Handle "before" and "after" indeterminate values.
-     */
-    static Temporal getBeginning(final Period p) {
-        final var t = p.getBeginning();
-        return (t != null) ? t.getPosition() : null;
-    }
-
-    /**
-     * Returns the ending of the given period, or {@code null} if 
indeterminate.
-     *
-     * @param  p the period from which to get the ending.
-     * @return ending of the given period, or {@code null} if indeterminate.
-     *
-     * @todo Handle "before" and "after" indeterminate values.
-     */
-    static Temporal getEnding(final Period p) {
-        final var t = p.getEnding();
-        return (t != null) ? t.getPosition() : null;
-    }
-
     /**
      * Returns {@code true} if {@code other} is non-null and the specified 
comparison evaluates to {@code true}.
      * This is a helper function for {@code evaluate(…)} methods 
implementations.
@@ -210,8 +186,29 @@ abstract class TemporalOperation<T> implements 
Serializable {
      * @return the result of performing the comparison identified by {@code 
test}.
      * @throws DateTimeException if the two objects cannot be compared.
      */
-    protected final boolean compare(final int test, final T self, final 
Temporal other) {
-        return (other != null) && comparators.compare(test, self, other);
+    protected final boolean compare(final int test, final T self, final 
Instant other) {
+        if (other != null) {
+            final Temporal position;
+            final Optional<IndeterminateValue> p = 
other.getIndeterminatePosition();
+            if (p.isPresent()) {
+                if (p.get() == IndeterminateValue.NOW) {
+                    position = comparators.now();
+                } else {
+                    switch (test) {
+                        case BEFORE: if (p.get() != IndeterminateValue.AFTER)  
return false; else break;
+                        case AFTER:  if (p.get() != IndeterminateValue.BEFORE) 
return false; else break;
+                        default: return false;
+                    }
+                    position = other.getPosition();
+                }
+            } else {
+                position = other.getPosition();
+            }
+            if (position != null) {
+                return comparators.compare(test, self, position);
+            }
+        }
+        return false;
     }
 
     /**
@@ -225,9 +222,34 @@ abstract class TemporalOperation<T> implements 
Serializable {
      * @throws DateTimeException if the two objects cannot be compared.
      */
     @SuppressWarnings("unchecked")
-    protected static boolean compare(final int test, final Temporal self, 
final Temporal other) {
-        return (self != null) && (other != null) && TimeMethods.compare(test,
-                (Class) Classes.findCommonClass(self.getClass(), 
other.getClass()), self, other);
+    protected static boolean compare(final int test, final Instant self, final 
Instant other) {
+        if (self == null || other == null) {
+            return false;
+        }
+        final IndeterminateValue p1 = 
self.getIndeterminatePosition().orElse(null);
+        final IndeterminateValue p2 = 
other.getIndeterminatePosition().orElse(null);
+        if (p1 != null || p2 != null) {
+            if (p1 == p2) {
+                return (test == EQUAL) && (p1 == IndeterminateValue.NOW);
+            }
+            switch (test) {
+                case BEFORE: if (isAmbiguous(p1, IndeterminateValue.BEFORE) || 
isAmbiguous(p2, IndeterminateValue.AFTER))  return false; else break;
+                case AFTER:  if (isAmbiguous(p1, IndeterminateValue.AFTER)  || 
isAmbiguous(p2, IndeterminateValue.BEFORE)) return false; else break;
+                default: return false;
+            }
+        }
+        return TimeMethods.compareAny(test, self.getPosition(), 
other.getPosition());
+    }
+
+    /**
+     * Returns {@code true} if using the {@code p} value would be ambiguous.
+     *
+     * @param  p         the indeterminate value to test.
+     * @param  required  the required value for a non-ambiguous comparison.
+     * @return whether using the given value would be ambiguous.
+     */
+    private static boolean isAmbiguous(final IndeterminateValue p, final 
IndeterminateValue required) {
+        return (p != null) && (p != IndeterminateValue.NOW) && (p != required);
     }
 
 
@@ -284,20 +306,20 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Extension to ISO 19108: handle instant as a tiny period. */
         @Override public boolean evaluate(T self, Period other) {
-            return compare(EQUAL, self, getBeginning(other)) &&
-                   compare(EQUAL, self, getEnding(other));
+            return compare(EQUAL, self, other.getBeginning()) &&
+                   compare(EQUAL, self, other.getEnding());
         }
 
         /** Extension to ISO 19108: handle instant as a tiny period. */
         @Override public boolean evaluate(Period self, T other) {
-            return compare(EQUAL, other, getBeginning(self)) &&
-                   compare(EQUAL, other, getEnding(self));
+            return compare(EQUAL, other, self.getBeginning()) &&
+                   compare(EQUAL, other, self.getEnding());
         }
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(Period self, Period other) {
-            return compare(EQUAL, getBeginning(self), getBeginning(other)) &&
-                   compare(EQUAL, getEnding(self),    getEnding(other));
+            return compare(EQUAL, self.getBeginning(), other.getBeginning()) &&
+                   compare(EQUAL, self.getEnding(),    other.getEnding());
         }
     }
 
@@ -338,17 +360,17 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Relationship not defined by ISO 19108:2006. */
         @Override public boolean evaluate(T self, Period other) {
-            return compare(BEFORE, self, getBeginning(other));
+            return compare(BEFORE, self, other.getBeginning());
         }
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(Period self, T other) {
-            return compare(AFTER, other, getEnding(self));
+            return compare(AFTER, other, self.getEnding());
         }
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(Period self, Period other) {
-            return compare(BEFORE, getEnding(self), getBeginning(other));
+            return compare(BEFORE, self.getEnding(), other.getBeginning());
         }
     }
 
@@ -389,17 +411,17 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Relationship not defined by ISO 19108:2006. */
         @Override public boolean evaluate(T self, Period other) {
-            return compare(AFTER, self, getEnding(other));
+            return compare(AFTER, self, other.getEnding());
         }
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(Period self, T other) {
-            return compare(BEFORE, other, getBeginning(self));
+            return compare(BEFORE, other, self.getBeginning());
         }
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(Period self, Period other) {
-            return compare(AFTER, getBeginning(self), getEnding(other));
+            return compare(AFTER, self.getBeginning(), other.getEnding());
         }
     }
 
@@ -428,8 +450,8 @@ abstract class TemporalOperation<T> implements Serializable 
{
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            return compare(EQUAL,  getBeginning(self), getBeginning(other)) &&
-                   compare(BEFORE, getEnding(self),    getEnding(other));
+            return compare(EQUAL,  self.getBeginning(), other.getBeginning()) 
&&
+                   compare(BEFORE, self.getEnding(),    other.getEnding());
         }
     }
 
@@ -458,8 +480,8 @@ abstract class TemporalOperation<T> implements Serializable 
{
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            return compare(EQUAL, getEnding(self),    getEnding(other)) &&
-                   compare(AFTER, getBeginning(self), getBeginning(other));
+            return compare(EQUAL, self.getEnding(),    other.getEnding()) &&
+                   compare(AFTER, self.getBeginning(), other.getBeginning());
         }
     }
 
@@ -489,13 +511,13 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(Period self, T other) {
-            return compare(EQUAL, other, getBeginning(self));
+            return compare(EQUAL, other, self.getBeginning());
         }
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            return compare(EQUAL, getBeginning(self), getBeginning(other)) &&
-                   compare(AFTER, getEnding(self),    getEnding(other));
+            return compare(EQUAL, self.getBeginning(), other.getBeginning()) &&
+                   compare(AFTER, self.getEnding(),    other.getEnding());
         }
     }
 
@@ -525,13 +547,13 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final T other) {
-            return compare(EQUAL, other, getEnding(self));
+            return compare(EQUAL, other, self.getEnding());
         }
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            return compare(EQUAL,  getEnding(self),    getEnding(other)) &&
-                   compare(BEFORE, getBeginning(self), getBeginning(other));
+            return compare(EQUAL,  self.getEnding(),    other.getEnding()) &&
+                   compare(BEFORE, self.getBeginning(), other.getBeginning());
         }
     }
 
@@ -565,17 +587,17 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Extension to ISO 19108: handle instant as a tiny period. */
         @Override public boolean evaluate(final T self, final Period other) {
-            return compare(EQUAL, self, getBeginning(other));
+            return compare(EQUAL, self, other.getBeginning());
         }
 
         /** Extension to ISO 19108: handle instant as a tiny period. */
         @Override public boolean evaluate(final Period self, final T other) {
-            return compare(EQUAL, other, getEnding(self));
+            return compare(EQUAL, other, self.getEnding());
         }
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            return compare(EQUAL, getEnding(self), getBeginning(other));
+            return compare(EQUAL, self.getEnding(), other.getBeginning());
         }
     }
 
@@ -609,17 +631,17 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Extension to ISO 19108: handle instant as a tiny period. */
         @Override public boolean evaluate(final T self, final Period other) {
-            return compare(EQUAL, self, getEnding(other));
+            return compare(EQUAL, self, other.getEnding());
         }
 
         /** Extension to ISO 19108: handle instant as a tiny period. */
         @Override public boolean evaluate(final Period self, final T other) {
-            return compare(EQUAL, other, getBeginning(self));
+            return compare(EQUAL, other, self.getBeginning());
         }
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            return compare(EQUAL, getBeginning(self), getEnding(other));
+            return compare(EQUAL, self.getBeginning(), other.getEnding());
         }
     }
 
@@ -653,8 +675,8 @@ abstract class TemporalOperation<T> implements Serializable 
{
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            return compare(AFTER,  getBeginning(self), getBeginning(other)) &&
-                   compare(BEFORE, getEnding(self),    getEnding(other));
+            return compare(AFTER,  self.getBeginning(), other.getBeginning()) 
&&
+                   compare(BEFORE, self.getEnding(),    other.getEnding());
         }
     }
 
@@ -689,14 +711,14 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final T other) {
-            return compare(AFTER,  other, getBeginning(self)) &&
-                   compare(BEFORE, other, getEnding(self));
+            return compare(AFTER,  other, self.getBeginning()) &&
+                   compare(BEFORE, other, self.getEnding());
         }
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            return compare(BEFORE, getBeginning(self), getBeginning(other)) &&
-                   compare(AFTER,  getEnding(self),    getEnding(other));
+            return compare(BEFORE, self.getBeginning(), other.getBeginning()) 
&&
+                   compare(AFTER,  self.getEnding(),    other.getEnding());
         }
     }
 
@@ -725,11 +747,11 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            final Temporal selfBegin, selfEnd, otherBegin, otherEnd;
-            return ((otherBegin = getBeginning(other)) != null) &&
-                   ((selfBegin  = getBeginning(self))  != null) && 
compare(BEFORE, selfBegin, otherBegin) &&
-                   ((selfEnd    = getEnding   (self))  != null) && 
compare(AFTER,  selfEnd,   otherBegin) &&
-                   ((otherEnd   = getEnding   (other)) != null) && 
compare(BEFORE, selfEnd,   otherEnd);
+            final Instant selfBegin, selfEnd, otherBegin, otherEnd;
+            return ((otherBegin = other.getBeginning()) != null) &&
+                   ((selfBegin  = self.getBeginning())  != null) && 
compare(BEFORE, selfBegin, otherBegin) &&
+                   ((selfEnd    = self.   getEnding())  != null) && 
compare(AFTER,  selfEnd,   otherBegin) &&
+                   ((otherEnd   = other.  getEnding())  != null) && 
compare(BEFORE, selfEnd,   otherEnd);
         }
     }
 
@@ -758,11 +780,11 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Condition defined by ISO 19108:2006 (corrigendum) §5.2.3.5. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            final Temporal selfBegin, selfEnd, otherBegin, otherEnd;
-            return ((selfBegin  = getBeginning(self))  != null) &&
-                   ((otherBegin = getBeginning(other)) != null) && 
compare(AFTER,  selfBegin,  otherBegin) &&
-                   ((otherEnd   = getEnding   (other)) != null) && 
compare(BEFORE, selfBegin,  otherEnd)   &&
-                   ((selfEnd    = getEnding   (self))  != null) && 
compare(AFTER,  selfEnd,    otherEnd);
+            final Instant selfBegin, selfEnd, otherBegin, otherEnd;
+            return ((selfBegin  = self.getBeginning())  != null) &&
+                   ((otherBegin = other.getBeginning()) != null) && 
compare(AFTER,  selfBegin,  otherBegin) &&
+                   ((otherEnd   = other.  getEnding())  != null) && 
compare(BEFORE, selfBegin,  otherEnd)   &&
+                   ((selfEnd    = self.   getEnding())  != null) && 
compare(AFTER,  selfEnd,    otherEnd);
         }
     }
 
@@ -789,11 +811,11 @@ abstract class TemporalOperation<T> implements 
Serializable {
 
         /** Condition defined by OGC filter specification. */
         @Override public boolean evaluate(final Period self, final Period 
other) {
-            final Temporal selfBegin, selfEnd, otherBegin, otherEnd;
-            return ((selfBegin  = getBeginning(self))  != null) &&
-                   ((otherEnd   = getEnding   (other)) != null) && 
compare(BEFORE, selfBegin, otherEnd) &&
-                   ((selfEnd    = getEnding   (self))  != null) &&
-                   ((otherBegin = getBeginning(other)) != null) && 
compare(AFTER, selfEnd, otherBegin);
+            final Instant selfBegin, selfEnd, otherBegin, otherEnd;
+            return ((selfBegin  = self.getBeginning())  != null) &&
+                   ((otherEnd   = other.  getEnding())  != null) && 
compare(BEFORE, selfBegin, otherEnd) &&
+                   ((selfEnd    = self.   getEnding())  != null) &&
+                   ((otherBegin = other.getBeginning()) != null) && 
compare(AFTER, selfEnd, otherBegin);
         }
     }
 }
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 52064eeb7a..2ebefb315d 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
@@ -18,7 +18,6 @@ package org.apache.sis.temporal;
 
 import java.util.Objects;
 import java.util.Optional;
-import java.io.Serializable;
 import java.time.Duration;
 import java.time.DateTimeException;
 import java.time.ZonedDateTime;
@@ -26,6 +25,7 @@ import java.time.temporal.Temporal;
 import java.time.temporal.TemporalAmount;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.resources.Errors;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
@@ -42,9 +42,13 @@ import org.opengis.temporal.IndeterminatePositionException;
  * This is not the same as {@link java.time.Instant}, because the
  * instant can actually be a date, or may be indeterminate.
  *
+ * <h2>Thread-safety</h2>
+ * Instances of this class are mostly immutable, except for the list of 
identifiers.
+ * All instances are thread-safe.
+ *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class DefaultInstant implements Instant, Serializable {
+final class DefaultInstant extends TemporalObject implements Instant {
     /**
      * For cross-version compatibility.
      */
@@ -226,6 +230,33 @@ cmp:    if (canTestBefore | canTestAfter | canTestEqual) {
         throw new 
IndeterminatePositionException(Errors.format(Errors.Keys.IndeterminatePosition));
     }
 
+    /**
+     * Compares this instant with the given object, optionally ignoring 
timezone.
+     * If the comparison mode ignores metadata, this method compares only the 
position on the timeline.
+     *
+     * @param  other  the object to compare to {@code this}.
+     * @param  mode   the strictness level of the comparison.
+     * @return {@code true} if both objects are equal according the given 
comparison mode.
+     */
+    @Override
+    public boolean equals(final Object object, final ComparisonMode mode) {
+        if (mode.equals(ComparisonMode.STRICT)) {   // Use `mode.equals(…)` 
for opportunistic null check.
+            return equals(object);
+        }
+        if (object instanceof Instant) {
+            final var that = (Instant) object;
+            if (indeterminate == that.getIndeterminatePosition().orElse(null)) 
{
+                if (indeterminate == IndeterminateValue.NOW || indeterminate 
== IndeterminateValue.UNKNOWN) {
+                    return true;
+                }
+                final Temporal other = that.getPosition();
+                return Objects.equals(position, other) ||       // Needed in 
all cases for testing null values.
+                        (mode.isIgnoringMetadata() && 
TimeMethods.compareAny(TimeMethods.EQUAL, position, other));
+            }
+        }
+        return false;
+    }
+
     /**
      * Compares this instant with the given object for equality.
      */
@@ -236,7 +267,9 @@ cmp:    if (canTestBefore | canTestAfter | canTestEqual) {
         }
         if (object instanceof DefaultInstant) {
             final var that = (DefaultInstant) object;
-            return Objects.equals(position, that.position) && indeterminate == 
that.indeterminate;
+            return indeterminate == that.indeterminate
+                    && Objects.equals(position, that.position)
+                    && equalIdentifiers(that);
         }
         return false;
     }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultPeriod.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultPeriod.java
index eb5dd82f5c..dff6b0edbd 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultPeriod.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/DefaultPeriod.java
@@ -16,8 +16,9 @@
  */
 package org.apache.sis.temporal;
 
-import java.io.Serializable;
 import java.time.temporal.TemporalAmount;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.Utilities;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.temporal.Instant;
@@ -27,9 +28,13 @@ import org.opengis.temporal.Period;
 /**
  * Default implementation of GeoAPI period.
  *
+ * <h2>Thread-safety</h2>
+ * Instances of this class are mostly immutable, except for the list of 
identifiers.
+ * All instances are thread-safe.
+ *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class DefaultPeriod implements Period, Serializable {
+final class DefaultPeriod extends TemporalObject implements Period {
     /**
      * For cross-version compatibility.
      */
@@ -100,7 +105,30 @@ final class DefaultPeriod implements Period, Serializable {
         }
         if (obj instanceof DefaultPeriod) {
             final var other = (DefaultPeriod) obj;
-            return beginning.equals(other.beginning) && 
ending.equals(other.ending);
+            return beginning.equals(other.beginning)
+                   && ending.equals(other.ending)
+                   && equalIdentifiers(other);
+        }
+        return false;
+    }
+
+    /**
+     * Compares this period with the given object, optionally ignoring 
timezone.
+     * If the comparison mode ignores metadata, this method compares only the 
position on the timeline.
+     *
+     * @param  other  the object to compare to {@code this}.
+     * @param  mode   the strictness level of the comparison.
+     * @return {@code true} if both objects are equal according the given 
comparison mode.
+     */
+    @Override
+    public boolean equals(final Object object, final ComparisonMode mode) {
+        if (mode.equals(ComparisonMode.STRICT)) {   // Use `mode.equals(…)` 
for opportunistic null check.
+            return equals(object);
+        }
+        if (object instanceof Period) {
+            final var other = (Period) object;
+            return Utilities.deepEquals(beginning, other.getBeginning(), mode) 
&&
+                   Utilities.deepEquals(ending,    other.getEnding(),    mode);
         }
         return false;
     }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/GeneralDuration.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/GeneralDuration.java
index 882721f7e5..6cd1724710 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/GeneralDuration.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/GeneralDuration.java
@@ -80,6 +80,20 @@ public final class GeneralDuration implements 
TemporalAmount, Serializable {
         this.time   = time;
     }
 
+    /**
+     * Returns the duration for the given components.
+     * If any component is zero, the other component is returned.
+     *
+     * @param  period  the period.
+     * @param  time    the component.
+     * @return the temporal amount from the given components.
+     */
+    public static TemporalAmount of(final Period period, final Duration time) {
+        if (period.isZero()) return time;
+        if (time.isZero()) return period;
+        return new GeneralDuration(period, time);
+    }
+
     /**
      * Parses a temporal amount which may contain a period and a duration part.
      * This method returns a {@link Period} or {@link Duration} if those 
objects
@@ -103,11 +117,8 @@ public final class GeneralDuration implements 
TemporalAmount, Serializable {
                     if (previousLetter == 'P') {
                         return Duration.parse(text);
                     }
-                    var period   = Period.parse(text.subSequence(0, i));
-                    var duration = Duration.parse(new StringBuilder(length - i 
+ 1).append('P').append(text, i, length));
-                    if (duration.isZero()) return period;
-                    if  (period.isZero())  return duration;
-                    return new GeneralDuration(period, duration);
+                    return of(Period.parse(text.subSequence(0, i)),
+                            Duration.parse(new StringBuilder(length - i + 
1).append('P').append(text, i, length)));
                 }
                 previousLetter = c;
             }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TemporalObject.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TemporalObject.java
new file mode 100644
index 0000000000..d0b2801d21
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/temporal/TemporalObject.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.temporal;
+
+import java.util.List;
+import java.util.Collection;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.io.Serializable;
+import org.opengis.metadata.Identifier;
+import org.apache.sis.util.LenientComparable;
+import org.apache.sis.xml.IdentifiedObject;
+import org.apache.sis.xml.IdentifierMap;
+import org.apache.sis.xml.bind.ModifiableIdentifierMap;
+
+
+/**
+ * Base class of temporal objects. This class allows to associate identifiers 
to this temporal object.
+ * The list of identifiers is modifiable because identifiers often need to be 
added after creation time,
+ * for example in order to associate {@code gml:id} during XML unmarshalling.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+abstract class TemporalObject implements IdentifiedObject, LenientComparable, 
Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 5408766446198380089L;
+
+    /**
+     * The identifier for this temporal object, or {@code null} if not yet 
created.
+     */
+    private CopyOnWriteArrayList<Identifier> identifiers;
+
+    /**
+     * Creates a new temporal object with no identifier.
+     */
+    TemporalObject() {
+    }
+
+    /**
+     * Returns all identifiers associated to this temporal object.
+     *
+     * @return all identifiers associated to this object, or an empty 
collection if none.
+     */
+    @Override
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    public final synchronized Collection<Identifier> getIdentifiers() {
+        if (identifiers == null) {
+            identifiers = new CopyOnWriteArrayList<>();
+        }
+        return identifiers;
+    }
+
+    /**
+     * Returns map view of the identifiers collection as 
(<var>authority</var>, <var>code</var>) entries.
+     *
+     * @return the identifiers as a map of (<var>authority</var>, 
<var>code</var>) entries, or an empty map if none.
+     */
+    @Override
+    public final IdentifierMap getIdentifierMap() {
+        return new ModifiableIdentifierMap(getIdentifiers());
+    }
+
+    /**
+     * Compares that identifiers of this temporal object with the identifiers 
of the given object.
+     */
+    final boolean equalIdentifiers(final TemporalObject that) {
+        List<Identifier> id1, id2;
+        synchronized (this) {id1 = this.identifiers;}
+        synchronized (that) {id2 = that.identifiers;}
+        if (id1 == null) id1 = List.of();
+        if (id2 == null) id2 = List.of();
+        return id1.equals(id2);
+    }
+}
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 a949c3d6cf..f6b71c4910 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
@@ -40,6 +40,7 @@ import java.time.temporal.TemporalAccessor;
 import java.lang.reflect.Modifier;
 import java.io.Serializable;
 import java.io.ObjectStreamException;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.privy.Strings;
 import org.apache.sis.util.resources.Errors;
 
@@ -159,6 +160,23 @@ public class TimeMethods<T> implements Serializable {
         return compareAsInstants(test, accessor(self), other);
     }
 
+    /**
+     * 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.
+     * The {@code compare(…)} methods should be preferred when the type is 
known in advance.
+     *
+     * @param  test   {@link #BEFORE}, {@link #AFTER} or {@link #EQUAL}.
+     * @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 result of performing the comparison identified by {@code 
test}.
+     * @throws DateTimeException if the two objects cannot be compared.
+     */
+    @SuppressWarnings("unchecked")
+    public static boolean compareAny(final int test, final Temporal self, 
final Temporal other) {
+        return (self != null) && (other != null)
+                && compare(test, (Class) 
Classes.findCommonClass(self.getClass(), other.getClass()), self, other);
+    }
+
     /**
      * 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
@@ -351,7 +369,7 @@ public class TimeMethods<T> implements Serializable {
      * @return the current time.
      * @throws ClassCastException if the {@linkplain #type} is {@link Date} or 
{@link MonthDay}.
      */
-    final Temporal now() {
+    public final Temporal now() {
         return (now != null) ? (Temporal) now.get() : ZonedDateTime.now();
     }
 
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/IdentifierMapAdapter.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/IdentifierMapAdapter.java
index 902ff0076e..cfd40d5ae3 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/IdentifierMapAdapter.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/IdentifierMapAdapter.java
@@ -460,6 +460,7 @@ public class IdentifierMapAdapter extends 
AbstractMap<Citation,String> implement
                 while (it.hasNext()) {
                     final Identifier identifier = it.next();
                     if (identifier != null) {
+                        @SuppressWarnings("LocalVariableHidesMemberVariable")
                         final Citation authority = identifier.getAuthority();
                         final Boolean state = put(authority, Boolean.FALSE);
                         if (state == null) {
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultExtentTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultExtentTest.java
index 63e08b0866..0c86b8dfe3 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultExtentTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/extent/DefaultExtentTest.java
@@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
 import org.apache.sis.metadata.xml.TestUsingFile;
 import static org.apache.sis.metadata.Assertions.assertXmlEquals;
+import static org.apache.sis.test.Assertions.assertEqualsIgnoreMetadata;
 
 
 /**
@@ -134,7 +135,7 @@ public final class DefaultExtentTest extends TestUsingFile {
         assertMarshalEqualsFile(openTestFile(format), extent, 
format.schemaVersion, STRICT,
                 new String[] {"gml:description"},                              
 // Ignored nodes.
                 new String[] {"xmlns:*", "xsi:schemaLocation", "gml:id"});     
 // Ignored attributes.
-        assertEquals(extent, unmarshalFile(DefaultExtent.class, 
openTestFile(format)));
+        assertEqualsIgnoreMetadata(extent, unmarshalFile(DefaultExtent.class, 
openTestFile(format)));
     }
 
     /**


Reply via email to