Author: desruisseaux Date: Thu Nov 29 16:36:09 2012 New Revision: 1415260 URL: http://svn.apache.org/viewvc?rev=1415260&view=rev Log: Initial commit of ISO 19139 NilReason.
Added: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObject.java (with props) sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java (with props) sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilReason.java (with props) sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/NilReasonTest.java (with props) sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/readme.txt (with props) Modified: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/util/Numbers.java sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java Modified: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/util/Numbers.java URL: http://svn.apache.org/viewvc/sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/util/Numbers.java?rev=1415260&r1=1415259&r2=1415260&view=diff ============================================================================== --- sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/util/Numbers.java (original) +++ sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/util/Numbers.java Thu Nov 29 16:36:09 2012 @@ -18,11 +18,20 @@ package org.apache.sis.util; import java.util.Map; import java.util.HashMap; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.Collections; +import java.lang.reflect.Array; import java.math.BigDecimal; import java.math.BigInteger; import org.apache.sis.util.resources.Errors; +import static org.apache.sis.util.collection.Collections.emptyQueue; +import static org.apache.sis.util.collection.Collections.emptySortedSet; + /** * Static methods working with {@link Number} objects, and a few primitive types by extension. @@ -590,6 +599,63 @@ public final class Numbers extends Stati } /** + * Returns a {@code NaN}, zero, empty or {@code null} value of the given type. This method + * tries to return the closest value that can be interpreted as "<cite>none</cite>", which + * is usually not the same than "<cite>zero</cite>". More specifically: + * + * <ul> + * <li>If the given type is a floating point <strong>primitive</strong> type ({@code float} + * or {@code double}), then this method returns {@link Float#NaN} or {@link Double#NaN} + * depending on the given type.</li> + * + * <li>If the given type is an integer <strong>primitive</strong> type or the character type + * ({@code long}, {@code int}, {@code short}, {@code byte} or {@code char}), then this + * method returns the zero value of the given type.</li> + * + * <li>If the given type is the {@code boolean} <strong>primitive</strong> type, then this + * method returns {@link Boolean#FALSE}.</li> + * + * <li>If the given type is an array or a collection, then this method returns an empty + * array or collection. The given type is honored on a <cite>best effort</cite> basis.</li> + * + * <li>For all other cases, including the wrapper classes of primitive types, this method + * returns {@code null}.</li> + * </ul> + * + * Despite being defined in the {@code Numbers} class, the scope of this method has been + * extended to array and collection types because those objects can also be seen as + * mathematical concepts. + * + * @param <T> The compile-time type of the requested object. + * @param type The type of the object for which to get a nil value. + * @return An object of the given type which represents a nil value, or {@code null}. + * + * @see org.apache.sis.xml.NilObject + */ + @SuppressWarnings("unchecked") + public static <T> T valueOfNil(final Class<T> type) { + final Numbers mapping = MAPPING.get(type); + if (mapping != null) { + if (type.isPrimitive()) { + return (T) mapping.nullValue; + } + } else if (type != null && type != Object.class) { + if (type == Map .class) return (T) Collections.EMPTY_MAP; + if (type == List .class) return (T) Collections.EMPTY_LIST; + if (type == Queue .class) return (T) emptyQueue(); + if (type == SortedSet.class) return (T) emptySortedSet(); + if (type.isAssignableFrom(Set.class)) { + return (T) Collections.EMPTY_SET; + } + final Class<?> element = type.getComponentType(); + if (element != null) { + return (T) Array.newInstance(element, 0); + } + } + return null; + } + + /** * Returns one of {@link #DOUBLE}, {@link #FLOAT}, {@link #LONG}, {@link #INTEGER}, * {@link #SHORT}, {@link #BYTE}, {@link #CHARACTER}, {@link #BOOLEAN} or {@link #OTHER} * constants for the given type. This is a commodity for usage in {@code switch} statements. Added: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObject.java URL: http://svn.apache.org/viewvc/sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObject.java?rev=1415260&view=auto ============================================================================== --- sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObject.java (added) +++ sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObject.java Thu Nov 29 16:36:09 2012 @@ -0,0 +1,73 @@ +/* + * 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; + + +/** + * A marker interface for empty XML elements. Note that an "nil" XML element may not be an + * empty Java object, since the Java object can still be associated with {@link XLink} or + * {@link NilReason} attributes. Those attributes are not part of ISO 19115, but may appear + * in ISO 19139 XML documents like below: + * + * <blockquote><table class="sis" border="1"><tr> + * <th>Non-empty {@code Series} element</th> + * <th>Empty {@code Series} element</th> + * </tr><tr><td> + * {@preformat xml + * <gmd:CI_Citation> + * <gmd:series> + * <gmd:CI_Series> + * <!-- Some content here --> + * </gmd:CI_Series> + * </gmd:series> + * </gmd:CI_Citation> + * } + * </td><td> + * {@preformat xml + * <gmd:CI_Citation> + * <gmd:series nilReason="unknown"/> + * </gmd:CI_Citation> + * } + * </td></tr></table></blockquote> + * + * The reason why an object is empty can be obtained by the {@link #getNilReason()} method. + * + * {@section Instantiation} + * The following example instantiates a {@link org.opengis.metadata.citation.Citation} object + * which is empty because the information are missing: + * + * {@preformat java + * Citation nil = NilReason.MISSING.createNilObject(Citation.class); + * } + * + * @author Martin Desruisseaux (Geomatys) + * @since 0.3 (derived from geotk-3.18) + * @version 0.3 + * @module + * + * @see NilReason#createNilObject(Class) + * @see ObjectLinker#resolve(Class, NilReason) + * @see org.apache.sis.util.Numbers#valueOfNil(Class) + */ +public interface NilObject { + /** + * Returns the reason why this object is empty. + * + * @return The reason why this object is empty. + */ + NilReason getNilReason(); +} Propchange: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObject.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObject.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java URL: http://svn.apache.org/viewvc/sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java?rev=1415260&view=auto ============================================================================== --- sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java (added) +++ sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java Thu Nov 29 16:36:09 2012 @@ -0,0 +1,229 @@ +/* + * 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.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import java.lang.reflect.Proxy; +import java.lang.reflect.Method; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; + +import org.opengis.metadata.Identifier; + +import org.apache.sis.util.Classes; +import org.apache.sis.util.Numbers; +import org.apache.sis.util.ComparisonMode; +import org.apache.sis.util.LenientComparable; +import org.apache.sis.util.resources.Errors; +import org.apache.sis.internal.jaxb.IdentifierMapAdapter; + + +/** + * The handler for an object where all methods returns null or empty collections, except + * a few methods related to object identity. This handler is used only when no concrete + * definition were found for a XML element identified by {@code xlink} or {@code uuidref} + * attributes. + * + * {@note The same handler could be used for every proxy having the same XLink. + * For now, it doesn't seem worth to cache the handlers.} + * + * @author Martin Desruisseaux (Geomatys) + * @since 0.3 (derived from geotk-3.18) + * @version 0.3 + * @module + */ +final class NilObjectHandler implements InvocationHandler { + /** + * The identifiers as an {@link IdentifierMapAdapter} object, or the {@code nilReason} + * attribute as a {@link NilReason} object. We don't use separated fields because + * those attributes are exclusive, and some operations like {@code toString()}, + * {@code hashCode()} and {@code equals(Object)} are the same for both types. + */ + private final Object attribute; + + /** + * Creates a new handler for an object identified by the given identifiers. + * The identifiers are wrapped in a mutable list, so users can add, remove + * or modify identifiers. + */ + NilObjectHandler(final Identifier[] identifiers) { + attribute = IdentifierMapAdapter.create(new ArrayList<>(Arrays.asList(identifiers))); + } + + /** + * Creates a new handler for an object which is nil for the given reason. + */ + NilObjectHandler(final NilReason nilReason) { + attribute = nilReason; + } + + /** + * Returns {@code true} if the given type is one of the interfaces ignored by + * {@link #getInterface(Object)}. + */ + static boolean isIgnoredInterface(final Class<?> type) { + return IdentifiedObject.class.isAssignableFrom(type) || + NilObject.class.isAssignableFrom(type) || + LenientComparable.class.isAssignableFrom(type); + } + + /** + * Returns the interface implemented by the given proxy. + */ + private static Class<?> getInterface(final Object proxy) { + for (final Class<?> type : proxy.getClass().getInterfaces()) { + if (!isIgnoredInterface(type)) { + return type; + } + } + throw new AssertionError(proxy); // Should not happen. + } + + /** + * Processes a method invocation. For any invocation of a getter method, there is a choice: + * + * <ul> + * <li>If the invoked method is {@code getIdentifiers()}, returns the identifiers given at + * construction time.</li> + * <li>If the invoked method is {@code getIdentifierMap()}, returns a view over the + * identifiers given at construction time.</li> + * <li>If the invoked method is any other kind of getter, returns null except if:<ul> + * <li>the return type is a collection, in which case an empty collection is returned;</li> + * <li>the return type is a primitive, in which case the nil value for that primitive + * type is returned.</li></ul></li> + * <li>If the invoked method is a setter method, throw a {@link UnsupportedOperationException} + * since the proxy instance is assumed unmodifiable.</li> + * <li>If the invoked method is one of the {@link Object} method, delegate to the + * {@link #reference}.</li> + * </ul> + */ + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + final String name = method.getName(); + if (args == null) { + switch (name) { + case "getNilReason": { + return (attribute instanceof NilReason) ? (NilReason) attribute : null; + } + case "getIdentifierMap": { + return (attribute instanceof IdentifierMap) ? (IdentifierMap) attribute : null; + } + case "getIdentifiers": { + return (attribute instanceof IdentifierMapAdapter) ? + ((IdentifierMapAdapter) attribute).identifiers : null; + } + case "toString": { + return getInterface(proxy).getSimpleName() + '[' + attribute + ']'; + } + case "hashCode": { + return ~attribute.hashCode(); + } + } + if (name.startsWith("get") || name.startsWith("is")) { + return Numbers.valueOfNil(method.getReturnType()); + } + } else switch (args.length) { + case 1: { + if (name.equals("equals")) { + return equals(proxy, args[0], ComparisonMode.STRICT); + } + if (name.startsWith("set")) { + throw new UnsupportedOperationException(Errors.format( + Errors.Keys.UnmodifiableObject_1, getInterface(proxy))); + } + break; + } + case 2: { + if (name.equals("equals")) { + return equals(proxy, args[0], (ComparisonMode) args[1]); + } + break; + } + } + throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnsupportedOperation_1, + getInterface(proxy).getSimpleName() + '.' + name)); + } + + /** + * Compares the given objects to the given level of strictness. The first object shall + * be the proxy, and the second object an arbitrary implementation. This method returns + * {@code true} if the given arbitrary implementation is empty. + */ + private boolean equals(final Object proxy, final Object other, final ComparisonMode mode) throws Throwable { + if (other == proxy) return true; + if (other == null) return false; + if (proxy.getClass() == other.getClass()) { + if (mode.ordinal() >= ComparisonMode.IGNORE_METADATA.ordinal()) { + return true; + } + final NilObjectHandler h = (NilObjectHandler) Proxy.getInvocationHandler(other); + return attribute.equals(h.attribute); + } + switch (mode) { + case STRICT: return false; // The above test is the only relevant one for this mode. + case BY_CONTRACT: { + Object tx = attribute, ox = null; + if (tx instanceof IdentifierMapAdapter) { + tx = ((IdentifierMapAdapter) tx).identifiers; + if (other instanceof IdentifiedObject) { + ox = ((IdentifiedObject) other).getIdentifiers(); + } + } else { + if (other instanceof NilObject) { + ox = ((NilObject) other).getNilReason(); + } + } + if (!Objects.equals(tx, ox)) { + return false; + } + break; + } + } + /* + * Having two objects declaring the same identifiers and implementing the same interface, + * ensures that all properties in the other objects are null or empty collections. + */ + final Class<?> type = getInterface(proxy); + if (!type.isInstance(other)) { + return false; + } + for (final Method getter : type.getMethods()) { + if (Classes.isPossibleGetter(getter)) { + final Object value; + try { + value = getter.invoke(other, (Object[]) null); + } catch (InvocationTargetException e) { + throw e.getTargetException(); + } + if (value != null) { + if ((value instanceof Collection<?>) && ((Collection<?>) value).isEmpty()) { + continue; // Empty collection, which is consistent with this proxy behavior. + } + if ((value instanceof Map<?,?>) && ((Map<?,?>) value).isEmpty()) { + continue; // Empty collection, which is consistent with this proxy behavior. + } + return false; + } + } + } + return true; + } +} Propchange: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilReason.java URL: http://svn.apache.org/viewvc/sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilReason.java?rev=1415260&view=auto ============================================================================== --- sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilReason.java (added) +++ sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilReason.java Thu Nov 29 16:36:09 2012 @@ -0,0 +1,344 @@ +/* + * 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.net.URI; +import java.net.URISyntaxException; +import java.io.Serializable; +import java.lang.reflect.Proxy; +import java.lang.reflect.InvocationHandler; +import net.jcip.annotations.Immutable; +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; + + +/** + * 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. + * + * @author Martin Desruisseaux (Geomatys) + * @since 0.3 (derived from geotk-3.18) + * @version 0.3 + * @module + * + * @see NilObject + */ +@Immutable +public final class NilReason implements Serializable { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = -1302619137838086028L; + + /** + * There is no value. + * + * <p>The string representation is {@code "inapplicable"}.</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"}.</p> + */ + public static final NilReason MISSING = new NilReason("missing"); + + /** + * The value will be available later. + * + * <p>The string representation is {@code "template"}.</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"}.</p> + */ + public static final NilReason UNKNOWN = new NilReason("unknown"); + + /** + * The value is not divulged. + * + * <p>The string representation is {@code "withheld"}.</p> + */ + public static final NilReason WITHHELD = new NilReason("withheld"); + + /** + * Other reason without explanation. Users are encouraged to use the {@link #valueOf(String)} + * method instead than this constant, in order to provide a brief explanation. + * + * <p>When testing if a {@code NilReason} is {@code "other"}, users should test if + * <code>{@linkplain #getExplanation()} != null</code> instead than comparing + * against this constant.</p> + * + * <p>The string representation of this constant is {@code "other"}. + * The string representation of other values created by {@code valueOf(String)} is + * {@code "other:text"} where {@code text} is a string of two or more characters + * with no included spaces.</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 an {@link URI}. + */ + private final Object reason; + + /** + * The invocation handler for empty objects, created when first needed. + * The same handler can be shared for all objects. + */ + private transient InvocationHandler handler; + + /** + * 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 a new instance is created and returned for that explanation. + * Note that if the given explanation contains any characters that are not + * {@linkplain Character#isUnicodeIdentifierPart(char) unicode identifier} + * (for example white spaces), then those characters are omitted.</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:"); + i++; // Skip the ':' character. + 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() == 6) { // 6 is the length of "other:" + 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. + */ + private Object readResolve() { + 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.</p> + * + * @return The explanation, or {@code null} if this {@code NilReason} is not of kind {@link #OTHER}. + */ + public String getExplanation() { + 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}. + * + * @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:text"}, 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 enum. + * This method returns an object which implement the given interface together with the + * {@link NilObject} interface. The {@link NilObject#getNilReason()} method will return + * this enum, and all other methods (except the ones inherited from the {@link Object} class) + * will return {@code null} or an empty collection as appropriate. + * + * @param <T> The compile-time type of the {@code type} argument. + * @param type The object type as an <strong>interface</strong>. + * This is usually a <a href="http://www.geoapi.org">GeoAPI</a> interface. + * @return An {@link NilObject} of the given type. + */ + @SuppressWarnings("unchecked") + public <T> T createNilObject(final Class<T> type) { + ArgumentChecks.ensureNonNull("type", type); + if (NilObjectHandler.isIgnoredInterface(type)) { + throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "type", type)); + } + InvocationHandler h; + synchronized (this) { + if ((h = handler) == null) { + handler = h = new NilObjectHandler(this); + } + } + return (T) Proxy.newProxyInstance(NilReason.class.getClassLoader(), + new Class<?>[] {type, NilObject.class, LenientComparable.class}, h); + } +} Propchange: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilReason.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: sis/branches/JDK7/sis-utility/src/main/java/org/apache/sis/xml/NilReason.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Modified: sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java URL: http://svn.apache.org/viewvc/sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java?rev=1415260&r1=1415259&r2=1415260&view=diff ============================================================================== --- sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java (original) +++ sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java Thu Nov 29 16:36:09 2012 @@ -72,6 +72,7 @@ import org.junit.runners.Suite; // XML most basic types. org.apache.sis.xml.XLinkTest.class, + org.apache.sis.xml.NilReasonTest.class, org.apache.sis.internal.jaxb.IdentifierMapAdapterTest.class, org.apache.sis.internal.jaxb.IdentifierMapWithSpecialCasesTest.class }) Added: sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/NilReasonTest.java URL: http://svn.apache.org/viewvc/sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/NilReasonTest.java?rev=1415260&view=auto ============================================================================== --- sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/NilReasonTest.java (added) +++ sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/NilReasonTest.java Thu Nov 29 16:36:09 2012 @@ -0,0 +1,138 @@ +/* + * 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.net.URISyntaxException; +import org.opengis.metadata.citation.Citation; +import org.opengis.metadata.citation.ResponsibleParty; +import org.apache.sis.util.LenientComparable; +import org.apache.sis.util.ComparisonMode; +import org.apache.sis.util.Arrays; +import org.apache.sis.test.TestCase; +import org.junit.*; + +import static org.apache.sis.test.Assert.*; + + +/** + * Tests the {@link NilReason}. + * + * @author Martin Desruisseaux (Geomatys) + * @since 0.3 (derived from geotk-3.18) + * @version 0.3 + * @module + */ +public final strictfp class NilReasonTest extends TestCase { + /** + * Tests the {@link NilReason#valueOf(String)} method on constants. + * + * @throws URISyntaxException Should never happen. + */ + @Test + public void testValueOfConstant() throws URISyntaxException { + assertSame(NilReason.TEMPLATE, NilReason.valueOf("template")); + assertSame(NilReason.MISSING, NilReason.valueOf("missing")); + assertSame(NilReason.TEMPLATE, NilReason.valueOf("TEMPLATE")); + assertSame(NilReason.MISSING, NilReason.valueOf(" missing ")); + + final NilReason[] reasons = NilReason.values(); + assertTrue(Arrays.contains(reasons, NilReason.TEMPLATE)); + assertTrue(Arrays.contains(reasons, NilReason.MISSING)); + } + + /** + * Tests the {@link NilReason#valueOf(String)} method on "other". + * + * @throws URISyntaxException Should never happen. + */ + @Test + public void testValueOfOther() throws URISyntaxException { + assertSame(NilReason.OTHER, NilReason.valueOf("other")); + final NilReason other = NilReason.valueOf("other:myReason"); + assertSame(other, NilReason.valueOf(" OTHER : myReason ")); + assertNotSame("Expected a new instance.", NilReason.OTHER, other); + assertFalse ("NilReason.equals(Object)", NilReason.OTHER.equals(other)); + assertEquals ("NilReason.getExplanation()", "myReason", other.getExplanation()); + assertNull ("NilReason.getURI()", other.getURI()); + + final NilReason[] reasons = NilReason.values(); + assertTrue(Arrays.contains(reasons, NilReason.TEMPLATE)); + assertTrue(Arrays.contains(reasons, NilReason.MISSING)); + assertTrue(Arrays.contains(reasons, other)); + } + + /** + * Tests the {@link NilReason#valueOf(String)} method on a URI. + * + * @throws URISyntaxException Should never happen. + */ + @Test + public void testValueOfURI() throws URISyntaxException { + final NilReason other = NilReason.valueOf("http://www.nilreasons.org"); + assertSame(other, NilReason.valueOf(" http://www.nilreasons.org ")); + assertNull ("NilReason.getExplanation()", other.getExplanation()); + assertEquals("NilReason.getURI()", "http://www.nilreasons.org", String.valueOf(other.getURI())); + + final NilReason[] reasons = NilReason.values(); + assertTrue(Arrays.contains(reasons, NilReason.TEMPLATE)); + assertTrue(Arrays.contains(reasons, NilReason.MISSING)); + assertTrue(Arrays.contains(reasons, other)); + } + + /** + * Tests the creation of {@link NilObject} instances. + */ + @Test + public void testCreateNilObject() { + final Citation citation = NilReason.TEMPLATE.createNilObject(Citation.class); + assertInstanceOf("Unexpected proxy.", NilObject.class, citation); + assertNull(citation.getTitle()); + assertTrue(citation.getDates().isEmpty()); + assertEquals("NilObject.toString()", "Citation[template]", citation.toString()); + } + + /** + * Tests the comparison of {@link NilObject} instances. + */ + @Test + public void testNilObjectComparison() { + final Citation e1 = NilReason.TEMPLATE.createNilObject(Citation.class); + final Citation e2 = NilReason.MISSING .createNilObject(Citation.class); + final Citation e3 = NilReason.TEMPLATE.createNilObject(Citation.class); + assertEquals("NilObject.hashCode()", e1.hashCode(), e3.hashCode()); + assertFalse ("NilObject.hashCode()", e1.hashCode() == e2.hashCode()); + assertEquals("NilObject.equals(Object)", e1, e3); + assertFalse ("NilObject.equals(Object)", e1.equals(e2)); + + assertInstanceOf("e1", LenientComparable.class, e1); + final LenientComparable c = (LenientComparable) e1; + assertTrue (c.equals(e3, ComparisonMode.STRICT)); + assertFalse(c.equals(e2, ComparisonMode.STRICT)); + assertFalse(c.equals(e2, ComparisonMode.BY_CONTRACT)); + assertTrue (c.equals(e2, ComparisonMode.IGNORE_METADATA)); + assertTrue (c.equals(e2, ComparisonMode.APPROXIMATIVE)); + assertTrue (c.equals(e2, ComparisonMode.DEBUG)); + + // Following object should alway be different because it does not implement the same interface. + final ResponsibleParty r1 = NilReason.TEMPLATE.createNilObject(ResponsibleParty.class); + assertFalse(c.equals(r1, ComparisonMode.STRICT)); + assertFalse(c.equals(r1, ComparisonMode.BY_CONTRACT)); + assertFalse(c.equals(r1, ComparisonMode.IGNORE_METADATA)); + assertFalse(c.equals(r1, ComparisonMode.APPROXIMATIVE)); + assertFalse(c.equals(r1, ComparisonMode.DEBUG)); + } +} Propchange: sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/NilReasonTest.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/NilReasonTest.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Added: sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/readme.txt URL: http://svn.apache.org/viewvc/sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/readme.txt?rev=1415260&view=auto ============================================================================== --- sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/readme.txt (added) +++ sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/readme.txt Thu Nov 29 16:36:09 2012 @@ -0,0 +1,2 @@ +Most tests are actually performed in the sis-metadata module, +since we need some concrete classes for running the tests. Propchange: sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/readme.txt ------------------------------------------------------------------------------ svn:eol-style = native Propchange: sis/branches/JDK7/sis-utility/src/test/java/org/apache/sis/xml/readme.txt ------------------------------------------------------------------------------ svn:mime-type = text/plain