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 <martin.desruisse...@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));