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