You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/11/18 17:05:40 UTC

(sis) branch geoapi-4.0 updated: Add support for nil date and nil URI in metadata.

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 3d379f9aad Add support for nil date and nil URI in metadata.
3d379f9aad is described below

commit 3d379f9aad63817f4b728bdcc76775126328245e
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sat Nov 18 17:50:51 2023 +0100

    Add support for nil date and nil URI in metadata.
---
 .../main/org/apache/sis/xml/NilDate.java           | 69 +++++++++++++++++
 .../org/apache/sis/xml/NilInternationalString.java |  2 +-
 .../main/org/apache/sis/xml/NilReason.java         | 47 ++++++------
 .../apache/sis/xml/bind/FinalClassExtensions.java  |  2 +-
 .../main/org/apache/sis/xml/package-info.java      |  2 +-
 .../test/org/apache/sis/xml/NilReasonTest.java     | 88 ++++++++++++++--------
 6 files changed, 149 insertions(+), 61 deletions(-)

diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilDate.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilDate.java
new file mode 100644
index 0000000000..047603685f
--- /dev/null
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilDate.java
@@ -0,0 +1,69 @@
+/*
+ * 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.xml;
+
+import java.util.Date;
+import java.io.ObjectStreamException;
+
+
+/**
+ * An empty {@code Date} which is nil for the given reason.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class NilDate extends Date implements NilObject {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 4374532826187673813L;
+
+    /**
+     * The reason why the object is nil.
+     */
+    private final NilReason reason;
+
+    /**
+     * Creates a new international string which is nil for the given reason.
+     */
+    NilDate(final NilReason reason) {
+        super(0);
+        this.reason = reason;
+    }
+
+    /**
+     * Returns the reason why this object is nil.
+     */
+    @Override
+    public NilReason getNilReason() {
+        return reason;
+    }
+
+    /**
+     * Unconditionally returns en empty string.
+     */
+    @Override
+    public String toString() {
+        return "";
+    }
+
+    /**
+     * Invoked on deserialization for replacing the deserialized instance by the unique instance.
+     */
+    private Object readResolve() throws ObjectStreamException {
+        return reason.createNilObject(Date.class);
+    }
+}
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilInternationalString.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilInternationalString.java
index d216921f5f..b902732366 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilInternationalString.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilInternationalString.java
@@ -24,7 +24,7 @@ import org.apache.sis.util.resources.Errors;
 
 
 /**
- * An empty {@link InternationalString} which is nil for the given reason.
+ * An empty {@code InternationalString} which is nil for the given reason.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilReason.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilReason.java
index c701f94242..25310a03be 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilReason.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilReason.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.xml;
 
+import java.util.Date;
 import java.util.Map;
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -55,7 +56,7 @@ import org.apache.sis.math.MathFunctions;
  * This final class is immutable and thus inherently thread-safe.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see NilObject
  *
@@ -427,7 +428,7 @@ public final class NilReason implements Serializable {
      *             {@code 0} or {@code false}, in this preference order, depending on the method return type.</li>
      *       </ul>
      *   </li>
-     *   <li>One of {@link Float}, {@link Double} or {@link String} types.
+     *   <li>One of {@link Float}, {@link Double}, {@link String}, {@link URI} or {@link Date} types.
      *       In such case, this method returns an instance which will be recognized as "nil" by the XML marshaller.</li>
      * </ul>
      *
@@ -445,25 +446,21 @@ public final class NilReason implements Serializable {
      * @return an {@link NilObject} of the given type.
      */
     @SuppressWarnings("unchecked")
-    public synchronized <T> T createNilObject(final Class<T> type) {
+    public <T> T createNilObject(final Class<T> type) {
         ArgumentChecks.ensureNonNull("type", type);
         /*
          * Check for existing instance in the cache before to create a new object. Returning a unique
          * instance is mandatory for the types handled by `createNilInstance(Class)`. Since we have
          * to cache those values anyway, we opportunistically extend the caching to other types too.
-         *
-         * Implementation note: we have two synchronizations here: one lock on `this` because of the
-         * `synchronized` statement in this method signature, and another lock in `WeakValueHashMap`.
-         * The second lock may seem useless since we already hold a lock on `this`. But it is actually
-         * needed because the garbage-collected entries are removed from the map in a background thread
-         * (see ReferenceQueueConsumer), which is synchronized on the map itself. It is better to keep
-         * the synchronization on the map shorter than the snychronization on `this` because a single
-         * ReferenceQueueConsumer thread is shared by all the SIS library.
          */
-        if (nilObjects == null) {
-            nilObjects = new WeakValueHashMap<>((Class) Class.class);
+        Map<Class<?>, Object> instances;
+        synchronized (this) {
+            instances = nilObjects;
+            if (instances == null) {
+                nilObjects = instances = new WeakValueHashMap<>((Class) Class.class);
+            }
         }
-        Object object = nilObjects.get(type);
+        Object object = instances.get(type);
         if (object == null) {
             /*
              * If no object has been previously created, check for the usual case where the requested type
@@ -490,9 +487,8 @@ public final class NilReason implements Serializable {
                  */
                 object = createNilInstance(type);
             }
-            if (nilObjects.put(type, object) != null) {
-                throw new AssertionError(type);                                 // Should never happen.
-            }
+            final Object current = instances.putIfAbsent(type, object);
+            if (current != null) object = current;
         }
         return type.cast(object);
     }
@@ -503,12 +499,6 @@ public final class NilReason implements Serializable {
      * <p><b>Reminder:</b> If more special cases are added, do not forget to update the {@link #forObject(Object)}
      * method and to update the {@link #createNilObject(Class)} and {@link #forObject(Object)} javadoc.</p>
      *
-     * <h4>Implementation note</h4>
-     * There is no special case for {@link Character} because Java {@code char}s are not really full Unicode characters.
-     * They are parts of UTF-16 encoding instead. If there is a need to represent a single Unicode character, we should
-     * probably still use a {@link String} where the string contain 1 or 2 Java characters. This may also facilitate the
-     * encoding in the XML files, since many files use another encoding than UTF-16 anyway.
-     *
      * @throws IllegalArgumentException if the given type is not a supported type.
      *
      * @see <a href="https://issues.apache.org/jira/browse/SIS-586">SIS-586</a>
@@ -519,7 +509,11 @@ public final class NilReason implements Serializable {
         if (type == Float  .class) return Float .valueOf(MathFunctions.toNanFloat(ordinal()));
         final Object object;
         if (type == String .class) {
-            object = new String("");         // REALLY need a new instance.
+            object = new String("");                // REALLY need a new instance.
+        } else if (type == URI.class) {
+            object = URI.create("");                // Really need a new instance.
+        } else if (type == Date.class) {
+            object = new NilDate(this);
         } else {
             throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "type", type));
         }
@@ -534,7 +528,8 @@ public final class NilReason implements Serializable {
      * <ul>
      *   <li>If the given object implements the {@link NilObject} interface, then this method delegates
      *       to the {@link NilObject#getNilReason()} method.</li>
-     *   <li>Otherwise if the given object is one of the {@link Float}, {@link Double} or {@link String} instances
+     *   <li>Otherwise if the given object is one of the {@link Float}, {@link Double}, {@link String},
+     *       {@link URI} or {@link Date} instances
      *       returned by {@link #createNilObject(Class)}, then this method returns the associated reason.</li>
      *   <li>Otherwise this method returns {@code null}.</li>
      * </ul>
@@ -560,7 +555,7 @@ public final class NilReason implements Serializable {
                 if (value.isNaN()) {
                     return forNumber(value);
                 }
-            } else if (object instanceof String) {
+            } else if (object instanceof String || object instanceof URI) {
                 return (NilReason) FinalClassExtensions.property(object);
             }
         }
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/FinalClassExtensions.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/FinalClassExtensions.java
index ada8d9a7e3..6631c07c58 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/FinalClassExtensions.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/FinalClassExtensions.java
@@ -58,7 +58,7 @@ public final class FinalClassExtensions {
      * does not provides any map implementation which is both an {@code IdentityHashMap} and a {@code WeakHashMap}.
      *
      * For now we do not use weak references. This means that if a user creates a custom {@code NilReason} by a call
-     * to {@link NilReason#valueOf(String)} and if (s)he uses that nil reason for a primitive type, then that custom
+     * to {@link NilReason#valueOf(String)} and if (s)he uses that nil reason for a final class, then that custom
      * {@code NilReason} instance and its sentinel values will never be garbage-collected.
      * We presume that such cases will be rare enough for not being an issue in practice.
      *
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/package-info.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/package-info.java
index 2d575a1536..0af032a3d2 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/package-info.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/package-info.java
@@ -59,7 +59,7 @@
  * @author  Guilhem Legal (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Cullen Rombach (Image Matters)
- * @version 1.4
+ * @version 1.5
  * @since   0.3
  */
 package org.apache.sis.xml;
diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java
index 1ac8564526..513ded6721 100644
--- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sis.xml;
 
+import java.net.URI;
+import java.util.Date;
 import java.net.URISyntaxException;
 import org.opengis.util.InternationalString;
 import org.opengis.metadata.citation.Citation;
@@ -27,8 +29,7 @@ import org.apache.sis.util.ArraysExt;
 import org.junit.Test;
 import org.apache.sis.test.TestCase;
 
-import static org.junit.Assert.*;
-import static org.opengis.test.Assert.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.*;
 
 // Specific to the geoapi-3.1 and geoapi-4.0 branches:
 import org.opengis.metadata.citation.Responsibility;
@@ -85,10 +86,10 @@ public final class NilReasonTest extends TestCase {
         assertSame(NilReason.OTHER, NilReason.valueOf("other"));
         final NilReason other = NilReason.valueOf("other:myReason");
         assertSame(other, NilReason.valueOf("  OTHER : myReason "));
-        assertNotSame("Expected a new instance.", NilReason.OTHER, other);
-        assertFalse  ("NilReason.equals(Object)", NilReason.OTHER.equals(other));
-        assertEquals ("NilReason.getOtherExplanation()", "myReason", other.getOtherExplanation());
-        assertNull   ("NilReason.getURI()", other.getURI());
+        assertNotSame  (NilReason.OTHER, other, "Expected a new instance.");
+        assertNotEquals(NilReason.OTHER, other);
+        assertEquals("myReason", other.getOtherExplanation());
+        assertNull(other.getURI(), "NilReason.getURI()");
 
         final NilReason[] reasons = NilReason.values();
         assertTrue(ArraysExt.contains(reasons, NilReason.TEMPLATE));
@@ -105,8 +106,8 @@ public final class NilReasonTest extends TestCase {
     public void testValueOfURI() throws URISyntaxException {
         final NilReason other = NilReason.valueOf("http://www.nilreasons.org");
         assertSame(other, NilReason.valueOf("  http://www.nilreasons.org  "));
-        assertNull  ("NilReason.getOtherExplanation()", other.getOtherExplanation());
-        assertEquals("NilReason.getURI()", "http://www.nilreasons.org", String.valueOf(other.getURI()));
+        assertNull(other.getOtherExplanation());
+        assertEquals("http://www.nilreasons.org", String.valueOf(other.getURI()));
 
         final NilReason[] reasons = NilReason.values();
         assertTrue(ArraysExt.contains(reasons, NilReason.TEMPLATE));
@@ -124,10 +125,10 @@ public final class NilReasonTest extends TestCase {
         final Float value = NilReason.MISSING.createNilObject(Float.class);
         assertEquals (nan, value);
         assertNotSame(nan, value);
-        assertSame("NilReason.forObject(…)", NilReason.MISSING, NilReason.forObject(value));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(nan));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(0f));
-        assertSame("Expected cached value.", value, NilReason.MISSING.createNilObject(Float.class));
+        assertSame(NilReason.MISSING, NilReason.forObject(value));
+        assertNull(NilReason.forObject(nan));
+        assertNull(NilReason.forObject(0f));
+        assertSame(value, NilReason.MISSING.createNilObject(Float.class), "Expected cached value.");
     }
 
     /**
@@ -140,10 +141,10 @@ public final class NilReasonTest extends TestCase {
         final Double value = NilReason.TEMPLATE.createNilObject(Double.class);
         assertEquals (nan, value);
         assertNotSame(nan, value);
-        assertSame("NilReason.forObject(…)", NilReason.TEMPLATE, NilReason.forObject(value));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(nan));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(0.0));
-        assertSame("Expected cached value.", value, NilReason.TEMPLATE.createNilObject(Double.class));
+        assertSame(NilReason.TEMPLATE, NilReason.forObject(value));
+        assertNull(NilReason.forObject(nan));
+        assertNull(NilReason.forObject(0.0));
+        assertSame(value, NilReason.TEMPLATE.createNilObject(Double.class), "Expected cached value.");
     }
 
     /**
@@ -155,10 +156,10 @@ public final class NilReasonTest extends TestCase {
         final String value = NilReason.MISSING.createNilObject(String.class);
         assertEquals ("", value);
         assertNotSame("", value);
-        assertSame("NilReason.forObject(…)", NilReason.MISSING, NilReason.forObject(value));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(""));
-        assertNull("NilReason.forObject(…)", NilReason.forObject("null"));
-        assertSame("Expected cached value.", value, NilReason.MISSING.createNilObject(String.class));
+        assertSame(NilReason.MISSING, NilReason.forObject(value));
+        assertNull(NilReason.forObject(""));
+        assertNull(NilReason.forObject("null"));
+        assertSame(value, NilReason.MISSING.createNilObject(String.class), "Expected cached value.");
     }
 
     /**
@@ -169,9 +170,32 @@ public final class NilReasonTest extends TestCase {
     public void testCreateNilInternationalString() {
         final InternationalString value = NilReason.MISSING.createNilObject(InternationalString.class);
         assertEquals("", value.toString());
-        assertInstanceOf("Unexpected impl.", NilObject.class, value);
-        assertSame("NilReason.forObject(…)", NilReason.MISSING, NilReason.forObject(value));
-        assertSame("Expected cached value.", value, NilReason.MISSING.createNilObject(InternationalString.class));
+        assertInstanceOf(NilObject.class, value);
+        assertSame(NilReason.MISSING, NilReason.forObject(value));
+        assertSame(value, NilReason.MISSING.createNilObject(InternationalString.class), "Expected cached value.");
+    }
+
+    /**
+     * Tests {@link NilReason#createNilObject(Class)} for a date.
+     */
+    @Test
+    public void testCreateNilDate() {
+        final Date value = NilReason.TEMPLATE.createNilObject(Date.class);
+        assertEquals("", value.toString());
+        assertInstanceOf(NilObject.class, value);
+        assertSame(NilReason.TEMPLATE, NilReason.forObject(value));
+        assertSame(value, NilReason.TEMPLATE.createNilObject(Date.class), "Expected cached value.");
+    }
+
+    /**
+     * Tests {@link NilReason#createNilObject(Class)} for an URI.
+     */
+    @Test
+    public void testCreateNilURI() {
+        final URI value = NilReason.MISSING.createNilObject(URI.class);
+        assertEquals("", value.toString());
+        assertSame(NilReason.MISSING, NilReason.forObject(value));
+        assertSame(value, NilReason.MISSING.createNilObject(URI.class), "Expected cached value.");
     }
 
     /**
@@ -180,12 +204,12 @@ public final class NilReasonTest extends TestCase {
     @Test
     public void testCreateNilObject() {
         final Citation citation = NilReason.TEMPLATE.createNilObject(Citation.class);
-        assertInstanceOf("Unexpected proxy.", NilObject.class, citation);
+        assertInstanceOf(NilObject.class, citation);
         assertNull(citation.getTitle());
         assertTrue(citation.getDates().isEmpty());
-        assertEquals("NilObject.toString()", "Citation[template]", citation.toString());
-        assertSame("NilReason.forObject(…)", NilReason.TEMPLATE, NilReason.forObject(citation));
-        assertSame("Expected cached value.", citation, NilReason.TEMPLATE.createNilObject(Citation.class));
+        assertEquals("Citation[template]", citation.toString());
+        assertSame(NilReason.TEMPLATE, NilReason.forObject(citation));
+        assertSame(citation, NilReason.TEMPLATE.createNilObject(Citation.class), "Expected cached value.");
     }
 
     /**
@@ -196,12 +220,12 @@ public final class NilReasonTest extends TestCase {
         final Citation e1 = NilReason.TEMPLATE.createNilObject(Citation.class);
         final Citation e2 = NilReason.MISSING .createNilObject(Citation.class);
         final Citation e3 = NilReason.TEMPLATE.createNilObject(Citation.class);
-        assertEquals("NilObject.hashCode()", e1.hashCode(), e3.hashCode());
-        assertFalse ("NilObject.hashCode()", e1.hashCode() == e2.hashCode());
-        assertEquals("NilObject.equals(Object)", e1, e3);
-        assertFalse ("NilObject.equals(Object)", e1.equals(e2));
+        assertEquals(e1.hashCode(), e3.hashCode());
+        assertFalse (e1.hashCode() == e2.hashCode());
+        assertEquals(e1, e3);
+        assertFalse (e1.equals(e2));
 
-        assertInstanceOf("e1", LenientComparable.class, e1);
+        assertInstanceOf(LenientComparable.class, e1);
         final LenientComparable c = (LenientComparable) e1;
         assertTrue (c.equals(e3, ComparisonMode.STRICT));
         assertFalse(c.equals(e2, ComparisonMode.STRICT));