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/07/18 17:20:15 UTC

[sis] 02/02: Abandon NilObject support of primitive wrappers, except for the types supporting NaN values. This feature depended on primitive wrapper constructors that are deprecated for removal.

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

commit 3725429e48acd348ede5fd55e8caf6d14d37e049
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Jul 18 16:59:09 2023 +0200

    Abandon NilObject support of primitive wrappers, except for the types supporting NaN values.
    This feature depended on primitive wrapper constructors that are deprecated for removal.
    
    https://issues.apache.org/jira/browse/SIS-586
---
 ...peProperties.java => FinalClassExtensions.java} |  72 ++++----
 .../apache/sis/internal/jaxb/gco/GO_Boolean.java   |   2 +-
 .../apache/sis/internal/jaxb/gco/GO_Integer.java   |   2 +-
 .../apache/sis/internal/jaxb/gco/GO_Integer64.java |   2 +-
 .../apache/sis/internal/jaxb/gco/PropertyType.java |   7 +-
 .../DefaultFeatureCatalogueDescription.java        |   5 +-
 .../iso/content/DefaultImageDescription.java       |   5 +-
 .../iso/extent/AbstractGeographicExtent.java       |   6 +-
 .../iso/extent/DefaultGeographicBoundingBox.java   |  21 +--
 .../main/java/org/apache/sis/xml/NilReason.java    | 196 +++++++++++++++------
 .../apache/sis/xml/NilReasonMarshallingTest.java   |  67 +------
 .../java/org/apache/sis/xml/NilReasonTest.java     | 109 ++----------
 12 files changed, 214 insertions(+), 280 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/PrimitiveTypeProperties.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/FinalClassExtensions.java
similarity index 53%
rename from core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/PrimitiveTypeProperties.java
rename to core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/FinalClassExtensions.java
index a27bf1f424..fefa0b5305 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/PrimitiveTypeProperties.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/FinalClassExtensions.java
@@ -19,32 +19,41 @@ package org.apache.sis.internal.jaxb;
 import java.util.Map;
 import java.util.IdentityHashMap;
 import java.lang.reflect.Modifier;
+import org.apache.sis.util.Numbers;
 import org.apache.sis.xml.NilReason;
 
 
 /**
- * A workaround for attaching properties ({@code nilreason}, {@code href}, <i>etc.</i>) to primitive type wrappers.
- * The normal approach in SIS is to implement the {@link org.apache.sis.xml.NilObject} interface. However, we cannot
- * do so when the object is a final Java class like {@link Boolean}, {@link Integer}, {@link Double} or {@link String}.
+ * A workaround for attaching properties ({@code nilreason}, {@code href}, <i>etc.</i>) to final classes.
+ * The normal approach in SIS is to implement the {@link org.apache.sis.xml.NilObject} interface.
+ * However, we cannot do so when the object is a final Java class such as {@link String}.
  * This class provides a workaround using specific instances of those wrappers.
  *
+ * <h2>Historical note</h2>
+ * In previous Apache SIS releases (before 1.4), this class was used mostly for primitive type wrappers such as
+ * {@link Boolean}, {@link Byte}, {@link Short}, {@link Integer}, {@link Long}, {@link Float} and {@link Double}.
+ * Support for those types has been removed because it depends on {@code java.lang} constructors now marked as
+ * deprecated for removal.
+ *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.4
+ * @version 1.4
  *
  * @see NilReason#createNilObject(Class)
+ * @see <a href="https://issues.apache.org/jira/browse/SIS-586">SIS-586</a>
  *
  * @since 0.4
  */
-public final class PrimitiveTypeProperties {
+public final class FinalClassExtensions {
     /**
-     * The map where to store specific instances. Keys are instances of the primitive wrappers considered as nil.
-     * Values are the {@code NilReason} why the primitive is missing, or any other property we may want to attach.
+     * The map where to store specific instances. Keys are instances considered as nil.
+     * Values are the {@code NilReason} why the instance is missing,
+     * or any other property we may want to attach.
      *
      * <h4>Identity comparisons</h4>
      * We really need an identity hash map; using the {@code Object.equals(Object)} method is not allowed here.
-     * This is because "nil values" are real values. For example if the type is {@link Integer}, then the nil value
-     * is an {@code Integer} instance having the value 0. We don't want to consider every 0 integer value as nil,
-     * but only the specific {@code Integer} instance used as sentinel value for nil.
+     * This is because "nil values" are real values. For example if the type is {@link String}, then the nil value
+     * is a {@code String} instance having the value "". We don't want to consider every empty string value as nil,
+     * but only the specific {@code String} instance used as sentinel value for nil.
      *
      * <h4>Weak references</h4>
      * We cannot use weak value references, because we don't want the {@link NilReason} (the map value) to be lost
@@ -52,7 +61,7 @@ public final class PrimitiveTypeProperties {
      * 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 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 primitive type, 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.
      *
@@ -64,50 +73,51 @@ public final class PrimitiveTypeProperties {
     /**
      * Do not allow instantiation of this class.
      */
-    private PrimitiveTypeProperties() {
+    private FinalClassExtensions() {
     }
 
     /**
-     * Returns {@code true} if the given type is a valid key. This {@code PrimitiveTypeProperties}
+     * Returns {@code true} if the given type is a valid key. This {@code FinalClassExtensions}
      * class is a workaround to be used only for final classes on which we have no control.
      * Non-final classes shall implement {@link org.apache.sis.xml.NilObject} instead.
+     * Primitive wrappers are not allowed neither because they will become value objects.
      */
-    private static boolean isValidKey(final Object primitive) {
-        return Modifier.isFinal(primitive.getClass().getModifiers());
+    private static boolean isValidKey(final Class<?> type) {
+        return Modifier.isFinal(type.getModifiers()) && Numbers.getEnumConstant(type) == Numbers.OTHER;
     }
 
     /**
-     * Associates the given property to the given primitive.
-     * The {@code primitive} argument shall be a specific instance created by the {@code new} keyword, not
-     * a shared instance link {@link Boolean#FALSE} or the values returned by {@link Integer#valueOf(int)}.
+     * Associates the given property to the given instance.
+     * The {@code instance} argument shall be a specific instance created by the {@code new} keyword,
+     * not a shared instance like static constants or instances created by static factory methods.
      *
-     * @param  primitive  the {@link Boolean}, {@link Integer}, {@link Double} or {@link String} specific instance.
-     * @param  property   the {@link NilReason} or other property to associate to the given instance.
+     * @param  instance  the {@link String} (or other final class) specific instance.
+     * @param  property  the {@link NilReason} or other property to associate to the given instance.
      */
-    public static void associate(final Object primitive, final Object property) {
-        assert isValidKey(primitive) : primitive;
+    public static void associate(final Object instance, final Object property) {
+        assert isValidKey(instance.getClass()) : instance;
         synchronized (SENTINEL_VALUES) {
-            final Object old = SENTINEL_VALUES.put(primitive, property);
+            final Object old = SENTINEL_VALUES.put(instance, property);
             if (old != null) {                          // Should never happen - this is rather debugging check.
-                SENTINEL_VALUES.put(primitive, old);
-                throw new AssertionError(primitive);
+                SENTINEL_VALUES.put(instance, old);
+                throw new AssertionError(instance);
             }
         }
     }
 
     /**
-     * Returns the property of the given primitive type, or {@code null} if none.
+     * Returns the property of the given final type, or {@code null} if none.
      *
-     * @param  primitive  the {@link Boolean}, {@link Integer}, {@link Double} or {@link String} specific instance.
+     * @param  instance  the {@link String} (or other final class) specific instance.
      * @return the property associated to the given instance, or {@code null} if none.
      */
-    public static Object property(final Object primitive) {
+    public static Object property(final Object instance) {
         /*
-         * No 'assert isValidKey(primitive)' because this method is sometimes invoked
-         * only after a brief inspection (e.g. 'NilReason.mayBeNil(Object)' method).
+         * No `assert isValidKey(instance)` because this method is sometimes invoked
+         * only after a brief inspection (e.g. `NilReason.mayBeNil(Object)` method).
          */
         synchronized (SENTINEL_VALUES) {
-            return SENTINEL_VALUES.get(primitive);
+            return SENTINEL_VALUES.get(instance);
         }
     }
 }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Boolean.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Boolean.java
index 44a66f1dde..23bab631a6 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Boolean.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Boolean.java
@@ -47,7 +47,7 @@ public final class GO_Boolean extends PropertyType<GO_Boolean, Boolean> {
      */
     @SuppressWarnings("NumberEquality")
     private GO_Boolean(final Boolean value) {
-        super(value, !value && value != Boolean.FALSE);
+        super(value, false);
     }
 
     /**
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Integer.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Integer.java
index 118efa01fa..6d80c8628c 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Integer.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Integer.java
@@ -50,7 +50,7 @@ public class GO_Integer extends PropertyType<GO_Integer, Integer> {
      * @param  value  the value.
      */
     private GO_Integer(final Integer value) {
-        super(value, value == 0);
+        super(value, false);
     }
 
     /**
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Integer64.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Integer64.java
index 95132ef9b3..9b8b73c70a 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Integer64.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/GO_Integer64.java
@@ -43,7 +43,7 @@ public final class GO_Integer64 extends PropertyType<GO_Integer64, Long> {
      * @param  value  the value.
      */
     private GO_Integer64(final Long value) {
-        super(value, value == 0L);
+        super(value, false);
     }
 
     /**
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/PropertyType.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/PropertyType.java
index 03389f4272..fec1d09474 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/PropertyType.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/jaxb/gco/PropertyType.java
@@ -33,7 +33,6 @@ import org.apache.sis.xml.ReferenceResolver;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.internal.jaxb.Context;
 import org.apache.sis.internal.jaxb.FilterByVersion;
-import org.apache.sis.internal.jaxb.PrimitiveTypeProperties;
 import org.apache.sis.util.SimpleInternationalString;
 import org.apache.sis.util.resources.Errors;
 
@@ -146,7 +145,7 @@ public abstract class PropertyType<ValueType extends PropertyType<ValueType,Boun
     }
 
     /**
-     * Builds a {@code PropertyType} wrapper for the given primitive type wrapper.
+     * Builds a {@code PropertyType} wrapper for an instance of a final class.
      * This constructor checks for nil reasons only if {@code check} is {@code true}.
      *
      * @param  value     the primitive type wrapper.
@@ -155,8 +154,8 @@ public abstract class PropertyType<ValueType extends PropertyType<ValueType,Boun
     protected PropertyType(final BoundType value, final boolean mayBeNil) {
         metadata = value;
         if (mayBeNil) {
-            final Object property = PrimitiveTypeProperties.property(value);
-            if (property instanceof NilReason) {
+            final NilReason property = NilReason.forObject(value);
+            if (property != null) {
                 reference = property.toString();
                 metadata  = null;
             }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java
index 6523501792..331a127a5e 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java
@@ -83,10 +83,7 @@ public class DefaultFeatureCatalogueDescription extends AbstractContentInformati
 
     /**
      * Whether or not the cited feature catalogue complies with ISO 19110.
-     *
-     * <p>Implementation note: we need to store the reference to the {@code Boolean} instance instead
-     * than using bitmask because {@link org.apache.sis.internal.jaxb.PrimitiveTypeProperties} may
-     * associate some properties to that particular instance.</p>
+     * May be {@code null} is unspecified.
      */
     private Boolean compliant;
 
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultImageDescription.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultImageDescription.java
index 8fb228cbb9..37132a80eb 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultImageDescription.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultImageDescription.java
@@ -111,10 +111,7 @@ public class DefaultImageDescription extends DefaultCoverageDescription implemen
 
     /**
      * Indication of whether or not triangulation has been performed upon the image.
-     *
-     * <p>Implementation note: we need to store the reference to the {@code Boolean} instance instead
-     * than using bitmask because {@link org.apache.sis.internal.jaxb.PrimitiveTypeProperties} may
-     * associate some properties to that particular instance.</p>
+     * May be {@code null} is unspecified.
      */
     private Boolean triangulationIndicator;
 
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/AbstractGeographicExtent.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/AbstractGeographicExtent.java
index f0406057e7..d5ae75b890 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/AbstractGeographicExtent.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/AbstractGeographicExtent.java
@@ -61,10 +61,7 @@ public class AbstractGeographicExtent extends ISOMetadata implements GeographicE
     /**
      * Indication of whether the bounding polygon encompasses an area covered by the data
      * (<cite>inclusion</cite>) or an area where data is not present (<cite>exclusion</cite>).
-     *
-     * <p>Implementation note: we need to store the reference to the {@code Boolean} instance instead
-     * than using bitmask because {@link org.apache.sis.internal.jaxb.PrimitiveTypeProperties} may
-     * associate some properties to that particular instance.</p>
+     * May be {@code null} is unspecified.
      */
     private Boolean inclusion;
 
@@ -144,6 +141,7 @@ public class AbstractGeographicExtent extends ISOMetadata implements GeographicE
      * (<cite>inclusion</cite>) or an area where data is not present (<cite>exclusion</cite>).
      *
      * @return {@code true} for inclusion, or {@code false} for exclusion.
+     *         May be {@code null} is unspecified.
      */
     @Override
     @XmlElement(name = "extentTypeCode")
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java
index cbc3871e7d..b82f5e9308 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java
@@ -33,9 +33,7 @@ import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.metadata.ReferencingServices;
-import org.apache.sis.internal.jaxb.PrimitiveTypeProperties;
 import org.apache.sis.metadata.InvalidMetadataException;
-import org.apache.sis.xml.NilReason;
 
 import static java.lang.Double.doubleToLongBits;
 
@@ -248,27 +246,14 @@ public class DefaultGeographicBoundingBox extends AbstractGeographicExtent imple
     }
 
     /**
-     * Makes sure that the given inclusion is non-nil, then returns its value.
+     * Makes sure that the given inclusion is non-null, then returns its value.
      * If the given inclusion is {@code null}, then the default value is {@code true}.
      *
-     * @param  value  the {@link org.opengis.metadata.extent.GeographicBoundingBox#getInclusion()} value.
+     * @param  value  the {@link GeographicBoundingBox#getInclusion()} value.
      * @return the given value as a primitive type.
-     * @throws InvalidMetadataException if the given value is nil.
      */
-    @SuppressWarnings("NumberEquality")
     static boolean getInclusion(final Boolean value) throws InvalidMetadataException {
-        if (value == null) {
-            return true;
-        }
-        final boolean p = value;
-        /*
-         * (value == Boolean.FALSE) is an optimization for a common case avoiding PrimitiveTypeProperties check.
-         * DO NOT REPLACE BY `equals` OR `booleanValue()` — the exact reference value matter.
-         */
-        if (p || (value == Boolean.FALSE) || !(PrimitiveTypeProperties.property(value) instanceof NilReason)) {
-            return p;
-        }
-        throw new InvalidMetadataException(Errors.format(Errors.Keys.MissingValueForProperty_1, "inclusion"));
+        return (value == null) || value;
     }
 
     /**
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/xml/NilReason.java b/core/sis-metadata/src/main/java/org/apache/sis/xml/NilReason.java
index 72cde921b4..ccfb945061 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/xml/NilReason.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/xml/NilReason.java
@@ -30,7 +30,8 @@ import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.LenientComparable;
 import org.apache.sis.util.collection.WeakHashSet;
 import org.apache.sis.util.collection.WeakValueHashMap;
-import org.apache.sis.internal.jaxb.PrimitiveTypeProperties;
+import org.apache.sis.internal.jaxb.FinalClassExtensions;
+import org.apache.sis.math.MathFunctions;
 
 
 /**
@@ -139,9 +140,38 @@ public final class NilReason implements Serializable {
 
     /**
      * The pool of other nil reasons created up to date.
+     *
+     * @see #unique()
      */
     private static final WeakHashSet<NilReason> POOL = new WeakHashSet<>(NilReason.class);
 
+    /**
+     * The last {@linkplain #ordinal} value used in a {@code NilReason} instance.
+     * This is used for choosing the next value to assign to {@link #ordinal}.
+     * This value is usually equal to {@code PREDEFINED.length + POOL.size()}
+     * but may be different if some pool entries have been garbage collected.
+     * Accesses to this field shall be synchronized on {@link #POOL}.
+     */
+    private static int lastOrdinal;
+
+    /**
+     * A numerical identifier of this {@code NilReason}, or 0 if not yet assigned.
+     * This field is used for choosing a NaN value for floating point numbers.
+     * We want the same NaN to be used for {@code float} and {@code double} types.
+     * This value shall not be modified after {@code NilReason} publication.
+     *
+     * @see #ordinal()
+     * @see #forNumber(Number)
+     */
+    private transient int ordinal;
+    static {
+        int i = 0;
+        while (i < PREDEFINED.length) {
+            PREDEFINED[i].ordinal = ++i;
+        }
+        lastOrdinal = i;
+    }
+
     /**
      * Either the XML value as a {@code String} (including the explanation if the prefix
      * is "{@code other}", or a {@link URI}. Those types are serializable.
@@ -254,10 +284,33 @@ public final class NilReason implements Serializable {
                 if (result.equals(reason)) {
                     result = reason;                            // Use the existing instance.
                 }
-                return POOL.unique(new NilReason(result));
+                return new NilReason(result).unique();
+            }
+        }
+        return new NilReason(new URI(reason)).unique();
+    }
+
+    /**
+     * Returns a unique instance of this nil reason.
+     * If another instance has been created previously for the same reason,
+     * that other instance is returned. Otherwise {@code this} is returned.
+     *
+     * @return a unique instance of this nil reason.
+     */
+    private NilReason unique() {
+        synchronized (POOL) {
+            final NilReason instance = POOL.unique(this);
+            if (instance == this) {
+                if ((instance.ordinal = ++lastOrdinal) < 0) {
+                    /*
+                     * Overflow, but still accept to create this nil reason.
+                     * Maybe it will not be used for floating point numbers.
+                     */
+                    instance.ordinal = lastOrdinal = Integer.MIN_VALUE;     // Really MIN, not MAX.
+                }
             }
+            return instance;
         }
-        return POOL.unique(new NilReason(new URI(reason)));
     }
 
     /**
@@ -274,7 +327,21 @@ public final class NilReason implements Serializable {
                 }
             }
         }
-        return POOL.unique(this);
+        return unique();
+    }
+
+    /**
+     * Returns an ordinal value for this nil reason.
+     * Numbers start at 1.
+     *
+     * @return an ordinal value for this nil reason, numbered from 1.
+     * @throws ArithmeticException if there is too many {@code NilReason} instances.
+     *
+     * @see #forNumber(Number)
+     */
+    final int ordinal() {
+        if (ordinal > 0) return ordinal;
+        throw new ArithmeticException(Errors.format(Errors.Keys.IntegerOverflow_1, Integer.SIZE));
     }
 
     /**
@@ -360,11 +427,17 @@ 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 {@code Boolean}, {@link Byte}, {@link Short}, {@code Integer}, {@link Long}, {@link Float},
-     *       {@code Double} or {@code String} types: in such case, this method returns a specific instance which
-     *       will be recognized as "nil" by the XML marshaller.</li>
+     *   <li>One of {@link Float}, {@link Double} or {@link String} types.
+     *       In such case, this method returns an instance which will be recognized as "nil" by the XML marshaller.</li>
      * </ul>
      *
+     * <h4>Historical note</h4>
+     * In previous Apache SIS releases, this method recognized also {@code Boolean}, {@link Byte}, {@link Short},
+     * {@code Integer}, {@link Long}, {@link Float} and {@code Double} types in the same way as {@code String}.
+     * The support for those types has been removed in Apache SIS 1.4 (except for types supporting NaN values)
+     * because it depends on {@code java.lang} constructors now marked as deprecated for removal.
+     * See <a href="https://issues.apache.org/jira/browse/SIS-586">SIS-586</a> on JIRA issue tracker.
+     *
      * @param  <T>   the compile-time type of the {@code type} argument.
      * @param  type  the object type as an <strong>interface</strong>
      *         (usually a <a href="http://www.geoapi.org">GeoAPI</a> one) or one of the special types.
@@ -376,7 +449,7 @@ public final class NilReason implements Serializable {
         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 `createNilPrimitive(Class)`. Since we have
+         * 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
@@ -412,11 +485,10 @@ public final class NilReason implements Serializable {
                 }
             } else {
                 /*
-                 * If the requested type is not an interface, this is usually an error except for some
-                 * special cases: Boolean, Byte, Short, Integer, Long, Float, Double or String.
+                 * If the requested type is not an interface, this is usually an error except for some special cases:
+                 * Float, Double and String (Apache SIS 1.3 supported also: Boolean, Byte, Short, Integer and Long).
                  */
-                object = createNilPrimitive(type);
-                PrimitiveTypeProperties.associate(object, this);
+                object = createNilInstance(type);
             }
             if (nilObjects.put(type, object) != null) {
                 throw new AssertionError(type);                                 // Should never happen.
@@ -426,11 +498,9 @@ public final class NilReason implements Serializable {
     }
 
     /**
-     * Returns a new {@code Boolean}, {@link Byte}, {@link Short}, {@code Integer}, {@link Long},
-     * {@link Float}, {@code Double} or {@code String} instance to be considered as a nil value.
-     * The caller is responsible for registering the value in {@link PrimitiveTypeProperties}.
+     * Returns a new {@code Float}, {@code Double} or {@code String} instance to be considered as a nil value.
      *
-     * <p><b>Reminder:</b> If more special cases are added, do not forget to update the {@link #mayBeNil(Object)}
+     * <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>
@@ -440,41 +510,21 @@ public final class NilReason implements Serializable {
      * 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>
      */
-    @SuppressWarnings({"deprecation", "RedundantStringConstructorCall", "BooleanConstructorCall", "UnnecessaryBoxing"})
-    private static Object createNilPrimitive(final Class<?> type) {
-        if (type == String .class) return new String("");         // REALLY need a new instance.
-        if (type == Boolean.class) return new Boolean(false);     // REALLY need a new instance, not Boolean.FALSE.
-        if (type == Byte   .class) return new Byte((byte) 0);     // REALLY need a new instance, not Byte.valueOf(0).
-        if (type == Short  .class) return new Short((byte) 0);    // REALLY need a new instance, not Short.valueOf(0).
-        if (type == Integer.class) return new Integer(0);         // REALLY need a new instance, not Integer.valueOf(0).
-        if (type == Long   .class) return new Long(0);            // REALLY need a new instance, not Long.valueOf(0).
-        if (type == Float  .class) return new Float(Float.NaN);   // REALLY need a new instance, not Float.valueOf(…).
-        if (type == Double .class) return new Double(Double.NaN); // REALLY need a new instance, not Double.valueOf(…).
-        throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "type", type));
-    }
-
-    /**
-     * Returns {@code true} if the given object may be one of the sentinel values
-     * created by {@link #createNilPrimitive(Class)}. This method only checks the value.
-     * If this method returns {@code true}, then the caller still needs to check the actual instance using the
-     * {@link PrimitiveTypeProperties} class. The purpose of this method is to filter the values that cannot
-     * be sentinel values, in order to avoid the synchronization done by {@code PrimitiveTypeProperties}.
-     */
-    private static boolean mayBeNil(final Object object) {
-        // `instanceof` checks on instances of final classes are expected to be very fast.
-        if (object instanceof String)  return ((String) object).isEmpty();
-        if (object instanceof Boolean) return !((Boolean) object) && (object != Boolean.FALSE);
-        if (object instanceof Number) {
-            /*
-             * Following test may return false positives for Long, Float and Double types, but this is okay
-             * since the real check will be done by PrimitiveTypeProperties.  The purpose of this method is
-             * only to perform a cheap filtering. Note that this method relies on the fact that casting NaN
-             * to `int` produces 0.
-             */
-            return ((Number) object).intValue() == 0;
+    @SuppressWarnings({"RedundantStringConstructorCall", "UnnecessaryBoxing"})
+    private Object createNilInstance(final Class<?> type) {
+        if (type == Double .class) return Double.valueOf(MathFunctions.toNanFloat(ordinal()));
+        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.
+        } else {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "type", type));
         }
-        return false;
+        FinalClassExtensions.associate(object, this);
+        return object;
     }
 
     /**
@@ -484,9 +534,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 {@code Boolean}, {@link Byte}, {@link Short}, {@code Integer},
-     *       {@link Long}, {@link Float}, {@code Double} or {@code String} instances returned by
-     *       {@link #createNilObject(Class)}, then this method returns the associated reason.</li>
+     *   <li>Otherwise if the given object is one of the {@link Float}, {@link Double} or {@link String} instances
+     *       returned by {@link #createNilObject(Class)}, then this method returns the associated reason.</li>
      *   <li>Otherwise this method returns {@code null}.</li>
      * </ul>
      *
@@ -501,10 +550,49 @@ public final class NilReason implements Serializable {
         if (object != null) {
             if (object instanceof NilObject) {
                 return ((NilObject) object).getNilReason();
+            } else if (object instanceof Double) {
+                final Double value = (Double) object;
+                if (value.isNaN()) {
+                    return forNumber(value);
+                }
+            } else if (object instanceof Float) {
+                final Float value = (Float) object;
+                if (value.isNaN()) {
+                    return forNumber(value);
+                }
+            } else if (object instanceof String) {
+                return (NilReason) FinalClassExtensions.property(object);
             }
-            if (mayBeNil(object)) {
-                return (NilReason) PrimitiveTypeProperties.property(object);
+        }
+        return null;
+    }
+
+    /**
+     * Returns the nil reason for the ordinal value extracted from the given floating point value.
+     * Caller should have verified that the value is NaN before to invoke this method.
+     *
+     * @param  value  the floating point value for which to search the nil reason.
+     * @return the nil reason, or {@code null} if none has been found for the given ordinal.
+     *
+     * @see #ordinal()
+     */
+    private static NilReason forNumber(final Number value) {
+        try {
+            final int ordinal = MathFunctions.toNanOrdinal(value.floatValue());
+            if (ordinal >= 1) {
+                if (ordinal <= PREDEFINED.length) {
+                    return PREDEFINED[ordinal - 1];
+                }
+                synchronized (POOL) {
+                    for (final NilReason reason : POOL) {       // Should be a very small set, usually empty.
+                        if (reason.ordinal == ordinal) {
+                            return reason;
+                        }
+                    }
+                }
             }
+        } catch (IllegalArgumentException e) {
+            // Unsupported bit pattern. Ignore.
         }
         return null;
     }
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/xml/NilReasonMarshallingTest.java b/core/sis-metadata/src/test/java/org/apache/sis/xml/NilReasonMarshallingTest.java
index 4e087fc105..6f509f575a 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/xml/NilReasonMarshallingTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/xml/NilReasonMarshallingTest.java
@@ -20,8 +20,6 @@ import jakarta.xml.bind.JAXBException;
 import org.opengis.metadata.content.Band;
 import org.opengis.metadata.citation.Series;
 import org.opengis.metadata.citation.Citation;
-import org.opengis.metadata.spatial.Dimension;
-import org.opengis.metadata.quality.ConformanceResult;
 import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.xml.TestCase;
 import org.junit.Test;
@@ -81,71 +79,10 @@ public final class NilReasonMarshallingTest extends TestCase {
         assertEquals(citation, unmarshal(Citation.class, actual));
     }
 
-    /**
-     * Tests a missing boolean value. The {@link Boolean}, {@link Integer}, {@link Double} and {@link String}
-     * values are implemented as special cases in {@link NilReason}, because they are final classes on which
-     * we have no control.
-     *
-     * @throws JAXBException if an error occurred during (un)marshalling.
-     */
-    @Test
-    @DependsOnMethod("testMissing")
-    public void testMissingBoolean() throws JAXBException {
-        final String expected =
-                "<mdq:DQ_ConformanceResult xmlns:mdq=\"" + Namespaces.MDQ + '"' +
-                                         " xmlns:gco=\"" + Namespaces.GCO + "\">\n" +
-                "  <mdq:explanation>\n" +
-                "    <gco:CharacterString>An explanation</gco:CharacterString>\n" +
-                "  </mdq:explanation>\n" +
-                "  <mdq:pass gco:nilReason=\"missing\"/>\n" +
-                "</mdq:DQ_ConformanceResult>";
-
-        final ConformanceResult result = unmarshal(ConformanceResult.class, expected);
-        assertEquals("explanation", "An explanation", result.getExplanation().toString());
-
-        final Boolean pass = result.pass();
-        assertNotNull("Expected a sentinel value.", pass);
-        assertEquals ("Nil value shall be false.",  Boolean.FALSE, pass);
-        assertNotSame("Expected a sentinel value.", Boolean.FALSE, pass);
-        assertSame("nilReason", NilReason.MISSING, NilReason.forObject(pass));
-
-        final String actual = marshal(result);
-        assertXmlEquals(expected, actual, "xmlns:*");
-        assertEquals(result, unmarshal(ConformanceResult.class, actual));
-    }
-
-    /**
-     * Tests a missing integer value. The {@link Boolean}, {@link Integer}, {@link Double} and {@link String}
-     * values are implemented as special cases in {@link NilReason}, because they are final classes on which
-     * we have no control.
-     *
-     * @throws JAXBException if an error occurred during (un)marshalling.
-     */
-    @Test
-    @DependsOnMethod("testMissing")
-    @SuppressWarnings("UnnecessaryBoxing")
-    public void testMissingInteger() throws JAXBException {
-        final String expected =
-                "<msr:MD_Dimension xmlns:msr=\"" + Namespaces.MSR + '"' +
-                                 " xmlns:gco=\"" + Namespaces.GCO + "\">\n" +
-                "  <msr:dimensionSize gco:nilReason=\"unknown\"/>\n" +
-                "</msr:MD_Dimension>";
-
-        final Dimension result = unmarshal(Dimension.class, expected);
-
-        final Integer size = result.getDimensionSize();
-        assertNotNull("Expected a sentinel value.", size);
-        assertEquals ("Nil value shall be 0.",      Integer.valueOf(0), size);
-        assertNotSame("Expected a sentinel value.", Integer.valueOf(0), size);
-        assertSame("nilReason", NilReason.UNKNOWN, NilReason.forObject(size));
-
-        final String actual = marshal(result);
-        assertXmlEquals(expected, actual, "xmlns:*");
-        assertEquals(result, unmarshal(Dimension.class, actual));
-    }
-
     /**
      * Tests a missing double value.
+     * The {@link Float}, {@link Double} and {@link String} values are implemented as special
+     * cases in {@link NilReason}, because they are final classes on which we have no control.
      *
      * @throws JAXBException if an error occurred during (un)marshalling.
      */
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/xml/NilReasonTest.java b/core/sis-metadata/src/test/java/org/apache/sis/xml/NilReasonTest.java
index 7bdf76eb5e..f91c765adc 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/xml/NilReasonTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/xml/NilReasonTest.java
@@ -34,7 +34,7 @@ import static org.opengis.test.Assert.assertInstanceOf;
  * Tests the {@link NilReason}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.4
+ * @version 1.4
  * @since   0.3
  */
 public final class NilReasonTest extends TestCase {
@@ -44,6 +44,18 @@ public final class NilReasonTest extends TestCase {
     public NilReasonTest() {
     }
 
+    /**
+     * Verifies {@link NilReason#ordinal()} values.
+     */
+    @Test
+    public void verifyOrdinal() {
+        assertEquals(3, NilReason.TEMPLATE    .ordinal());
+        assertEquals(2, NilReason.MISSING     .ordinal());
+        assertEquals(1, NilReason.INAPPLICABLE.ordinal());
+        assertEquals(4, NilReason.UNKNOWN     .ordinal());
+        assertEquals(5, NilReason.WITHHELD    .ordinal());
+    }
+
     /**
      * Tests the {@link NilReason#valueOf(String)} method on constants.
      *
@@ -100,95 +112,6 @@ public final class NilReasonTest extends TestCase {
         assertTrue(ArraysExt.contains(reasons, other));
     }
 
-    /**
-     * Tests {@link NilReason#createNilObject(Class)} for a boolean type.
-     * Opportunistically tests {@link NilReason#forObject(Object)} with the created object.
-     *
-     * @since 0.4
-     */
-    @Test
-    public void testCreateNilBoolean() {
-        final Boolean value = NilReason.MISSING.createNilObject(Boolean.class);
-        assertEquals (Boolean.FALSE, value);
-        assertNotSame(Boolean.FALSE, value);
-        assertSame("NilReason.forObject(…)", NilReason.MISSING, NilReason.forObject(value));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(Boolean.FALSE));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(Boolean.TRUE));
-        assertSame("Expected cached value.", value, NilReason.MISSING.createNilObject(Boolean.class));
-    }
-
-    /**
-     * Tests {@link NilReason#createNilObject(Class)} for an integer type.
-     * Opportunistically tests {@link NilReason#forObject(Object)} with the created object.
-     *
-     * @since 0.4
-     */
-    @Test
-    public void testCreateNilInteger() {
-        final Integer zero  = 0;
-        final Integer value = NilReason.MISSING.createNilObject(Integer.class);
-        assertEquals (zero, value);
-        assertNotSame(zero, value);
-        assertSame("NilReason.forObject(…)", NilReason.MISSING, NilReason.forObject(value));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(zero));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(1));
-        assertSame("Expected cached value.", value, NilReason.MISSING.createNilObject(Integer.class));
-    }
-
-    /**
-     * Tests {@link NilReason#createNilObject(Class)} for a byte type.
-     * Opportunistically tests {@link NilReason#forObject(Object)} with the created object.
-     *
-     * @since 0.4
-     */
-    @Test
-    public void testCreateNilByte() {
-        final Byte zero  = 0;
-        final Byte value = NilReason.MISSING.createNilObject(Byte.class);
-        assertEquals (zero, value);
-        assertNotSame(zero, value);
-        assertSame("NilReason.forObject(…)", NilReason.MISSING, NilReason.forObject(value));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(zero));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(1));
-        assertSame("Expected cached value.", value, NilReason.MISSING.createNilObject(Byte.class));
-    }
-
-    /**
-     * Tests {@link NilReason#createNilObject(Class)} for a short type.
-     * Opportunistically tests {@link NilReason#forObject(Object)} with the created object.
-     *
-     * @since 0.4
-     */
-    @Test
-    public void testCreateNilShort() {
-        final Short zero  = 0;
-        final Short value = NilReason.MISSING.createNilObject(Short.class);
-        assertEquals (zero, value);
-        assertNotSame(zero, value);
-        assertSame("NilReason.forObject(…)", NilReason.MISSING, NilReason.forObject(value));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(zero));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(1));
-        assertSame("Expected cached value.", value, NilReason.MISSING.createNilObject(Short.class));
-    }
-
-    /**
-     * Tests {@link NilReason#createNilObject(Class)} for a long type.
-     * Opportunistically tests {@link NilReason#forObject(Object)} with the created object.
-     *
-     * @since 0.4
-     */
-    @Test
-    public void testCreateNilLong() {
-        final Long zero  = 0L;
-        final Long value = NilReason.MISSING.createNilObject(Long.class);
-        assertEquals (zero, value);
-        assertNotSame(zero, value);
-        assertSame("NilReason.forObject(…)", NilReason.MISSING, NilReason.forObject(value));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(zero));
-        assertNull("NilReason.forObject(…)", NilReason.forObject(1L));
-        assertSame("Expected cached value.", value, NilReason.MISSING.createNilObject(Long.class));
-    }
-
     /**
      * Tests {@link NilReason#createNilObject(Class)} for a float type.
      * Opportunistically tests {@link NilReason#forObject(Object)} with the created object.
@@ -216,13 +139,13 @@ public final class NilReasonTest extends TestCase {
     @Test
     public void testCreateNilDouble() {
         final Double nan  = Double.NaN;
-        final Double value = NilReason.MISSING.createNilObject(Double.class);
+        final Double value = NilReason.TEMPLATE.createNilObject(Double.class);
         assertEquals (nan, value);
         assertNotSame(nan, value);
-        assertSame("NilReason.forObject(…)", NilReason.MISSING, NilReason.forObject(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.MISSING.createNilObject(Double.class));
+        assertSame("Expected cached value.", value, NilReason.TEMPLATE.createNilObject(Double.class));
     }
 
     /**