blob: 91fdc53bf02921233d9bc7f3e8b4067d168c2329 [file] [log] [blame]
/*
* 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.Map;
import java.net.URI;
import java.net.URISyntaxException;
import java.io.Serializable;
import java.io.ObjectStreamException;
import java.lang.reflect.Proxy;
import java.lang.reflect.InvocationHandler;
import org.opengis.util.InternationalString;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.ArgumentChecks;
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;
/**
* Explanation for a missing XML element. The nil reason can be parsed and formatted as a
* string using the {@link #valueOf(String)} and {@link #toString()} methods respectively.
* The string can be either a {@link URI} or an enumeration value described below.
* More specifically, {@code NilReason} can be:
*
* <ul>
* <li>One of the predefined {@link #INAPPLICABLE}, {@link #MISSING}, {@link #TEMPLATE},
* {@link #UNKNOWN} or {@link #WITHHELD} enumeration values.</li>
* <li>The {@link #OTHER} enumeration value, or a new enumeration value formatted as
* {@code "other:"} concatenated with a brief textual explanation.</li>
* <li>A URI which should refer to a resource which describes the reason for the exception.</li>
* </ul>
*
* {@code NilReason} is used in a number of XML elements where it is necessary to permit
* one of the above values as an alternative to the primary element.
*
* <h2>Immutability and thread safety</h2>
* This final class is immutable and thus inherently thread-safe.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.4
*
* @see NilObject
*
* @since 0.3
* @module
*/
public final class NilReason implements Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 5553785821187789895L;
/**
* There is no value.
*
* <p>The string representation is {@code "inapplicable"}.
* Other properties (explanation and URI) are {@code null}.</p>
*/
public static final NilReason INAPPLICABLE = new NilReason("inapplicable");
/**
* The correct value is not readily available to the sender of this data.
* Furthermore, a correct value may not exist.
*
* <p>The string representation is {@code "missing"}.
* Other properties (explanation and URI) are {@code null}.</p>
*/
public static final NilReason MISSING = new NilReason("missing");
/**
* The value will be available later.
*
* <p>The string representation is {@code "template"}.
* Other properties (explanation and URI) are {@code null}.</p>
*/
public static final NilReason TEMPLATE = new NilReason("template");
/**
* The correct value is not known to, and not computable by, the sender of this data.
* However, a correct value probably exists.
*
* <p>The string representation is {@code "unknown"}.
* Other properties (explanation and URI) are {@code null}.</p>
*/
public static final NilReason UNKNOWN = new NilReason("unknown");
/**
* The value is not divulged.
*
* <p>The string representation is {@code "withheld"}.
* Other properties (explanation and URI) are {@code null}.</p>
*/
public static final NilReason WITHHELD = new NilReason("withheld");
/**
* The {@value} string.
*/
private static final String other = "other";
/**
* Other reason without explanation.
* The string representation of this constant is {@code "other"}.
* The explanation property is an empty string, and the URI is {@code null}.
*
* <h4>Providing an explanation</h4>
* Users are encouraged to use the {@link #valueOf(String)} method instead than this constant,
* in order to provide a brief explanation. The string representation for {@code valueOf(…)}
* is <code>"other:<var>explanation</var>"</code> where <var>explanation</var> is a string of
* two or more characters with no included spaces.
*
* <p>When testing if a {@code NilReason} instance is any kind of {@code "other"} reason,
* users should test if <code>{@linkplain #getOtherExplanation()} != null</code> instead
* than comparing the reference against this constant.</p>
*/
public static final NilReason OTHER = new NilReason(other);
/**
* List of predefined constants.
*/
private static final NilReason[] PREDEFINED = {
INAPPLICABLE, MISSING, TEMPLATE, UNKNOWN, WITHHELD, OTHER
};
/**
* The pool of other nil reasons created up to date.
*/
private static final WeakHashSet<NilReason> POOL = new WeakHashSet<>(NilReason.class);
/**
* Either the XML value as a {@code String} (including the explanation if the prefix
* is "{@code other}", or a {@link URI}.
*/
private final Object reason;
/**
* The invocation handler for {@link NilObject} instances, created when first needed.
* The same handler can be shared for all objects having the same {@code NilReason},
* no matter the interface they implement.
*/
private transient InvocationHandler handler;
/**
* The values created by {@link #createNilObject(Class)}. They are often instances of {@link NilObject},
* except for some JDK types like {@link String}, {@link Boolean} or {@link Integer} which are handled
* in a special way.
*/
private transient Map<Class<?>, Object> nilObjects;
/**
* Creates a new reason for the given XML value or the given URI.
*/
private NilReason(final Object reason) {
this.reason = reason;
}
/**
* Returns an array containing every instances of this type that have not yet been
* garbage collected. The first elements of the returned array are the constants
* defined in this class, in declaration order. All other elements are the instances
* created by the {@link #valueOf(String)} method, in no particular order.
*
* @return an array containing the instances of {@code NilReason}.
*/
public static NilReason[] values() {
final int predefinedCount = PREDEFINED.length;
NilReason[] reasons;
synchronized (POOL) {
reasons = POOL.toArray(new NilReason[predefinedCount + POOL.size()]);
}
int count = reasons.length;
while (count != 0 && reasons[count-1] == null) {
count--;
}
count += predefinedCount;
final NilReason[] source = reasons;
if (count != reasons.length) {
reasons = new NilReason[count];
}
System.arraycopy(source, 0, reasons, predefinedCount, count - predefinedCount);
System.arraycopy(PREDEFINED, 0, reasons, 0, predefinedCount);
return reasons;
}
/**
* Parses the given nil reason. This method accepts the following cases:
*
* <ul>
* <li>If the given argument is one of the {@code "inapplicable"}, {@code "missing"},
* {@code "template"}, {@code "unknown"}, {@code "withheld"} or {@code "other"}
* strings (ignoring cases and leading/trailing spaces), then the corresponding
* pre-defined constant is returned.</li>
* <li>Otherwise if the given argument is {@code "other:"} followed by an explanation
* text, then an instance for that explanation is returned. More specifically:
* <ul>
* <li>{@linkplain Character#isSpaceChar(int) Space characters} and
* {@linkplain Character#isISOControl(int) ISO controls} are silently omitted.</li>
* <li>If the remaining string has less than two characters, then the {@link #OTHER}
* constant is returned.</li>
* </ul>
* </li>
* <li>Otherwise this method attempts to parse the given argument as a {@link URI}.
* Such URI should refer to a resource which describes the reason for the exception.</li>
* </ul>
*
* This method returns existing instances when possible.
*
* @param reason the reason why an element is not present.
* @return the reason as a {@code NilReason} object.
* @throws URISyntaxException if the given string is not one of the predefined enumeration
* values and can not be parsed as a URI.
*/
public static NilReason valueOf(String reason) throws URISyntaxException {
reason = CharSequences.trimWhitespaces(reason);
int i = reason.indexOf(':');
if (i < 0) {
for (final NilReason candidate : PREDEFINED) {
if (reason.equalsIgnoreCase((String) candidate.reason)) {
return candidate;
}
}
} else {
final int lower = CharSequences.skipLeadingWhitespaces(reason, 0, i);
final int upper = CharSequences.skipTrailingWhitespaces(reason, lower, i);
if (reason.regionMatches(true, lower, other, 0, upper - lower)) {
final int length = reason.length();
final StringBuilder buffer = new StringBuilder(length).append(other);
while (i < length) {
final int c = reason.codePointAt(i);
if (!Character.isSpaceChar(c) && !Character.isISOControl(c)) {
buffer.appendCodePoint(c);
}
i += Character.charCount(c);
}
if (buffer.length() < other.length() + 2) {
return OTHER;
}
String result = buffer.toString();
if (result.equals(reason)) {
result = reason; // Use the existing instance.
}
return POOL.unique(new NilReason(result));
}
}
return POOL.unique(new NilReason(new URI(reason)));
}
/**
* Invoked after deserialization in order to return a unique instance if possible.
*
* @return a unique instance of the deserialized {@code NilReason}.
* @throws ObjectStreamException required by specification but should never be thrown.
*/
private Object readResolve() throws ObjectStreamException {
if (reason instanceof String) {
for (final NilReason candidate : PREDEFINED) {
if (reason.equals(candidate.reason)) {
return candidate;
}
}
}
return POOL.unique(this);
}
/**
* If this {@code NilReason} is an enumeration of kind {@link #OTHER}, returns the explanation
* text. Otherwise returns {@code null}. If non-null, then the explanation is a string without
* whitespace.
*
* <p>Note that in the special case where {@code this} nil reason is the {@link #OTHER}
* instance itself, then this method returns an empty string. For all other cases, the
* string contains at least two characters.</p>
*
* @return the explanation, or {@code null} if this {@code NilReason} is not of kind {@link #OTHER}.
*/
public String getOtherExplanation() {
if (reason instanceof String) {
final String text = (String) reason;
final int s = text.indexOf(':');
if (s >= 0) {
return text.substring(s + 1);
}
if (text.equals(other)) {
return "";
}
}
return null;
}
/**
* If the explanation of this {@code NilReason} is referenced by a URI, returns that URI.
* Otherwise returns {@code null}. The URI and the {@linkplain #getOtherExplanation() other
* explanation} attributes are mutually exclusive.
*
* @return the URI, or {@code null} if the explanation of this {@code NilReason} is not referenced by a URI.
*/
public URI getURI() {
return (reason instanceof URI) ? (URI) reason : null;
}
/**
* Returns the GML string representation of this {@code NilReason}. The returned string
* is a simple enumeration value (e.g. {@code "inapplicable"}) if this {@code NilReason}
* is one of the predefined constants, or a string of the form {@code "other:explanation"},
* or a URI.
*
* @return the GML string representation of this {@code NilReason}.
*/
@Override
public String toString() {
return reason.toString();
}
/**
* Returns a hash code value for this {@code NilReason}.
*/
@Override
public int hashCode() {
return reason.hashCode() ^ (int) serialVersionUID;
}
/**
* Compares this {@code NilReason} with the specified object for equality.
*
* @param other the object to compare with this {@code NilReason}.
*/
@Override
public boolean equals(final Object other) {
if (other instanceof NilReason) {
return reason.equals(((NilReason) other).reason);
}
return false;
}
/**
* Returns an object of the given type which is nil for the reason represented by this instance.
* The {@code type} argument can be one of the following cases:
*
* <ul class="verbose">
* <li>An <strong>interface</strong>: in such case, this method returns an object which implement the given
* interface together with the {@link NilObject} and {@link LenientComparable} interfaces:
* <ul>
* <li>The {@link NilObject#getNilReason()} method will return this {@code NilReason} instance.</li>
* <li>The {@code equals(…)} and {@code hashCode()} methods behave as documented in {@link LenientComparable}.</li>
* <li>The {@code toString()} method is unspecified (may contain debugging information).</li>
* <li>All other methods return an empty collections, empty arrays, {@code null}, {@link Double#NaN NaN},
* {@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>
* </ul>
*
* @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.
* @throws IllegalArgumentException if the given type is not a supported type.
* @return an {@link NilObject} of the given type.
*/
@SuppressWarnings("unchecked")
public synchronized <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 'createNilPrimitive(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 an other 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);
}
Object object = nilObjects.get(type);
if (object == null) {
/*
* If no object has been previously created, check for the usual case where the requested type
* is an interface. We still have a special case for InternationalString. For all other cases,
* we will rely on the proxy.
*/
if (type.isInterface()) {
if (NilObjectHandler.isIgnoredInterface(type)) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "type", type));
}
if (type == InternationalString.class) {
object = new NilInternationalString(this);
} else {
if (handler == null) {
handler = new NilObjectHandler(this);
}
object = Proxy.newProxyInstance(NilReason.class.getClassLoader(),
new Class<?>[] {type, NilObject.class, LenientComparable.class}, handler);
}
} 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.
*/
object = createNilPrimitive(type);
PrimitiveTypeProperties.associate(object, this);
}
if (nilObjects.put(type, object) != null) {
throw new AssertionError(type); // Should never happen.
}
}
return type.cast(object);
}
/**
* 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}.
*
* <p><b>REMINDER:</b> If more special cases are added, do not forget to update the {@link #mayBeNil(Object)}
* method and to update the {@link #createNilObject(Class)} and {@link #forObject(Object)} javadoc.</p>
*
* <div class="note"><b>Implementation note:</b>
* 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 an other encoding than UTF-16 anyway.</div>
*
* @throws IllegalArgumentException if the given type is not a supported type.
*/
@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 can not
* 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;
}
return false;
}
/**
* If the given object is nil, returns the reason why it does not contain information.
* This method performs the following choices:
*
* <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 this method returns {@code null}.</li>
* </ul>
*
* @param object the object for which to get the {@code NilReason}, or {@code null}.
* @return the reason why the given object contains no information, or {@code null} if the given object is not nil.
*
* @see NilObject#getNilReason()
*
* @since 0.4
*/
public static NilReason forObject(final Object object) {
if (object != null) {
if (object instanceof NilObject) {
return ((NilObject) object).getNilReason();
}
if (mayBeNil(object)) {
return (NilReason) PrimitiveTypeProperties.property(object);
}
}
return null;
}
}