org.geotoolkit.metadata.PropertyAccessor Maven / Gradle / Ivy
Show all versions of geotk-metadata Show documentation
/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2007-2011, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2011, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.metadata;
import java.util.Set;
import java.util.Map;
import java.util.Arrays;
import java.util.Locale;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.UndeclaredThrowableException;
import net.jcip.annotations.ThreadSafe;
import org.opengis.annotation.UML;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.util.XArrays;
import org.geotoolkit.util.Strings;
import org.geotoolkit.util.Utilities;
import org.geotoolkit.util.ComparisonMode;
import org.geotoolkit.util.collection.XCollections;
import org.geotoolkit.util.collection.CheckedCollection;
import org.geotoolkit.util.converter.Classes;
import org.geotoolkit.util.converter.Numbers;
import org.geotoolkit.util.converter.ObjectConverter;
import org.geotoolkit.util.converter.ConverterRegistry;
import org.geotoolkit.util.converter.NonconvertibleObjectException;
import org.geotoolkit.xml.IdentifiedObject;
import static org.geotoolkit.internal.InternalUtilities.floatEpsilonEqual;
/**
* The getter methods declared in a GeoAPI interface, together with setter methods (if any)
* declared in the Geotk implementation. An instance of {@code PropertyAccessor} gives access
* to all public attributes of an instance of a metadata object. It uses reflection for this
* purpose, a little bit like the Java Beans framework.
*
* This accessor groups the properties in two categories:
*
*
* - The standard properties defined by the GeoAPI (or other standard) interfaces.
* Those properties are the only one accessible by most methods in this class,
* except {@link #shallowEquals}, {@link #shallowCopy} and {@link #freeze}.
*
* - Extra properties defined by the {@link IdentifiedObject} interface. Those properties
* invisible in the ISO 19115 model, but appears in ISO 19139 XML marshalling. So we do
* the same in the Geotk implementation: invisible in map and tree view, but visible in
* XML marshalling.
*
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.19
*
* @since 2.4
* @module
*/
@ThreadSafe
final class PropertyAccessor {
/**
* The locale to use for changing the case of characters.
*/
private static final Locale LOCALE = Locale.US;
/**
* The prefix for getters on boolean values.
*/
private static final String IS = "is";
/**
* The prefix for getters (general case).
*/
private static final String GET = "get";
/**
* The prefix for setters.
*/
private static final String SET = "set";
/**
* Methods to exclude from {@link #getGetters}. They are method inherited from {@link Object}
* which may be declared explicitly in interfaces with a formal contract. Only no-argument
* methods having a non-void return value need to be declared in this list.
*/
private static final String[] EXCLUDES = {
"clone", "getClass", "hashCode", "toString"
};
/**
* Getters shared between many instances of this class. Two different implementations
* may share the same getters but different setters.
*/
private static final Map, Method[]> SHARED_GETTERS = new HashMap, Method[]>();
/**
* Additional getter to declare in every list of getter methods that do not already provide
* their own {@code getIdentifiers()} method. We handle this method specially because it is
* needed for XML marshalling in ISO 19139 compliant document, while not part of abstract
* ISO 19115 specification.
*
* @see IdentifiedObject#getIdentifiers()
*
* @since 3.19
*/
private static final Method EXTRA_GETTER;
static {
try {
EXTRA_GETTER = IdentifiedObject.class.getMethod("getIdentifiers", (Class>[]) null);
} catch (NoSuchMethodException e) {
throw new AssertionError(e); // Should never happen.
}
}
/**
* The implemented metadata interface.
*/
final Class> type;
/**
* The implementation class. The following condition must hold:
*
* {@preformat java
* type.isAssignableFrom(implementation);
* }
*/
final Class> implementation;
/**
* Number of {@link #getters} methods to use. This is either {@code getters.length}
* or {@code getters.length-1}, depending whatever the {@link #EXTRA_GETTER} method
* needs to be skipped or not.
*
* @since 3.19
*/
private final int standardCount, allCount;
/**
* The getter methods. This array should not contain any null element.
* They are the methods defined in the interface, not the implementation class.
*/
private final Method[] getters;
/**
* The corresponding setter methods, or {@code null} if none. This array must have
* the same length than {@link #getters}. For every {@code getters[i]} element,
* {@code setters[i]} is the corresponding setter or {@code null} if there is none.
*/
private final Method[] setters;
/**
* The JavaBeans property names. They are computed at construction time,
* {@linkplain String#intern interned} then cached. Those names are often
* the same than field names (at least in Geotk implementation), so it is
* reasonable to intern them in order to share {@code String} instances.
*/
private final String[] names;
/**
* The types of elements for the corresponding getter and setter methods. If a getter
* method returns a collection, then this is the type of elements in that collection.
* Otherwise this is the type of the returned value itself.
*
* Primitive types like {@code double} or {@code int} are converted to their wrapper types.
*
* This array may contain null values if the type of elements in a collection is unknown
* (i.e. the collection is not parameterized).
*/
private final Class>[] elementTypes;
/**
* Index of getter or setter for a given name. Original names are duplicated with the same name
* converted to lower cases according {@link #LOCALE} conventions, for case-insensitive searches.
* This map must be considered as immutable after construction.
*
* The keys in this map are both inferred from the method names and fetched from the UML
* annotations. Consequently the map may contains many entries for the same value if some
* method names are different than the UML identifiers.
*/
private final Map mapping;
/**
* The last converter used. This is remembered on the assumption that the same converter
* will often be reused for the same property. This optimization can reduce the cost of
* looking for a converter, and also reduce thread contention since it reduce the number
* of calls to the synchronized {@link ConverterRegistry#converter} method.
*/
private transient volatile ObjectConverter,?> converter;
/**
* The restrictions that apply on property values. The array will be created when first
* needed. A {@link ValueRestriction#PENDING} element means that the restriction at that
* index has not yet been computed. If a property has been determined to have no
* restriction, then its corresponding element in this array is set to {@code null}.
*/
private transient ValueRestriction[] restrictions;
/**
* Creates a new property accessor for the specified metadata implementation.
*
* @param metadata The metadata implementation to wrap.
* @param type The interface implemented by the metadata.
* Shall be the value returned by {@link #getStandardType}.
*/
PropertyAccessor(final Class> implementation, final Class> type) {
this.implementation = implementation;
this.type = type;
assert type.isAssignableFrom(implementation) : implementation;
getters = getGetters(type);
int allCount = getters.length;
int standardCount = allCount;
if (allCount != 0 && getters[allCount-1] == EXTRA_GETTER) {
if (!EXTRA_GETTER.getDeclaringClass().isAssignableFrom(implementation)) {
allCount--; // The extra getter method does not exist.
}
standardCount--;
}
this.allCount = allCount;
this.standardCount = standardCount;
/*
* Compute all information derived from getters: setters, property names, value types.
*/
mapping = new HashMap(XCollections.hashMapCapacity(allCount));
names = new String[allCount];
elementTypes = new Class>[allCount];
Method[] setters = null;
final Class>[] arguments = new Class>[1];
for (int i=0; i returnType = getter.getReturnType();
arguments[0] = returnType;
if (name.length() > base) {
final char lo = name.charAt(base);
final char up = Character.toUpperCase(lo);
if (lo != up) {
name = SET + up + name.substring(base + 1);
} else {
name = SET + name.substring(base);
}
}
/*
* Note: we want PUBLIC methods only. For example the referencing module defines
* setters as private methods for use by JAXB only. We don't want to allow access
* to those setters.
*/
Method setter = null;
try {
setter = implementation.getMethod(name, arguments);
} catch (NoSuchMethodException e) {
/*
* If we found no setter method expecting an argument of the same type than the
* argument returned by the GeoAPI method, try again with the type returned by
* the implementation class. It is typically the same type, but sometime it may
* be a subtype.
*
* It is a necessary condition that the type returned by the getter is assignable
* to the type expected by the setter. This contract is required by the 'freeze'
* method among others.
*/
try {
getter = implementation.getMethod(getter.getName(), (Class>[]) null);
} catch (NoSuchMethodException error) {
// Should never happen, since the implementation class
// implements the interface where the getter come from.
throw new AssertionError(error);
}
if (returnType != (returnType = getter.getReturnType())) {
arguments[0] = returnType;
try {
setter = implementation.getMethod(name, arguments);
} catch (NoSuchMethodException ignore) {
// There is no setter, which may be normal. At this stage
// the 'setter' variable should still have the null value.
}
}
}
if (setter != null) {
if (setters == null) {
setters = new Method[allCount];
}
setters[i] = setter;
}
/*
* Get the type of elements returned by the getter. We perform this step last because
* the search for a setter above may have replaced the getter declared in the interface
* by the getter declared in the implementation with a covariant return type. Our intend
* is to get a type which can be accepted by the setter.
*/
Class> elementType = getter.getReturnType();
if (Collection.class.isAssignableFrom(elementType)) {
elementType = Classes.boundOfParameterizedAttribute(getter);
}
elementTypes[i] = Numbers.primitiveToWrapper(elementType);
}
this.setters = setters;
}
/**
* Adds the given (name, index) pair to {@link #mapping}, making sure we don't
* overwrite an existing entry with different value.
*/
private void addMapping(String name, final Integer index) throws IllegalArgumentException {
if (!name.isEmpty()) {
String original;
do {
final Integer old = mapping.put(name, index);
if (old != null && !old.equals(index)) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.PARAMETER_NAME_CLASH_$4, name, index, name, old));
}
original = name;
name = name.toLowerCase(LOCALE).trim();
} while (!name.equals(original));
}
}
/**
* Returns the metadata interface implemented by the specified implementation type.
* Only one metadata interface can be implemented. If the given type is already an
* interface from the standard, it is returned directly.
*
* @param type The type of the implementation (could also be the interface type).
* @param interfacePackage The root package for metadata interfaces.
* @return The single interface, or {@code null} if none where found.
*/
static Class> getStandardType(Class> type, final String interfacePackage) {
if (type != null) {
if (type.isInterface()) {
if (type.getName().startsWith(interfacePackage)) {
return type;
}
} else {
/*
* Gets every interfaces from the supplied package in declaration order,
* including the ones declared in the super-class.
*/
final Set> interfaces = new LinkedHashSet>();
do {
getInterfaces(type, interfacePackage, interfaces);
type = type.getSuperclass();
} while (type != null);
/*
* If we found more than one interface, removes the
* ones that are sub-interfaces of the other.
*/
for (final Iterator> it=interfaces.iterator(); it.hasNext();) {
final Class> candidate = it.next();
for (final Class> child : interfaces) {
if (candidate != child && candidate.isAssignableFrom(child)) {
it.remove();
break;
}
}
}
final Iterator> it=interfaces.iterator();
if (it.hasNext()) {
final Class> candidate = it.next();
if (!it.hasNext()) {
return candidate;
}
// Found more than one interface; we don't know which one to pick.
// Returns 'null' for now; the caller will thrown an exception.
}
}
}
return null;
}
/**
* Puts every interfaces for the given type in the specified collection.
* This method invokes itself recursively for scanning parent interfaces.
*/
private static void getInterfaces(final Class> type, final String interfacePackage,
final Collection> interfaces)
{
for (final Class> candidate : type.getInterfaces()) {
if (candidate.getName().startsWith(interfacePackage)) {
interfaces.add(candidate);
}
getInterfaces(candidate, interfacePackage, interfaces);
}
}
/**
* Returns the getters. The returned array should never be modified,
* since it may be shared among many instances of {@code PropertyAccessor}.
*
* @param type The metadata interface.
* @return The getters declared in the given interface (never {@code null}).
*/
private static Method[] getGetters(final Class> type) {
synchronized (SHARED_GETTERS) {
Method[] getters = SHARED_GETTERS.get(type);
if (getters == null) {
getters = type.getMethods();
boolean hasExtraGetter = false;
int count = 0;
for (int i=0; i base) {
if (isAcronym(name, base)) {
name = name.substring(base);
} else {
final char up = name.charAt(base);
final char lo = Character.toLowerCase(up);
if (up != lo) {
name = lo + name.substring(base + 1);
} else {
name = name.substring(base);
}
}
}
return name.trim().intern();
}
/**
* Returns the number of properties that can be read.
*/
final int count() {
return standardCount;
}
/**
* Returns the index of the specified property, or -1 if none.
* The search is case-insensitive.
*
* @param key The property to search.
* @return The index of the given key, or -1 if none.
*/
final int indexOf(final String name) {
Integer index = mapping.get(name);
if (index == null) {
/*
* Make a second try with lower cases only if the first try failed, because
* most of the time the key name will have exactly the expected case and using
* directly the given String instance allow usage of its cached hash code value.
*/
final String key = name.replace(" ", "").toLowerCase(LOCALE).trim();
if (key == name || (index = mapping.get(key)) == null) { // NOSONAR: identity comparison is okay here.
return -1;
}
}
return index;
}
/**
* Always returns the index of the specified property (never -1).
* The search is case-insensitive.
*
* @param key The property to search.
* @return The index of the given key.
* @throws IllegalArgumentException if the given key is not found.
*/
final int requiredIndexOf(final String key) throws IllegalArgumentException {
final int index = indexOf(key);
if (index >= 0) {
return index;
}
throw new IllegalArgumentException(Errors.format(Errors.Keys.UNKNOWN_PARAMETER_NAME_$1, key));
}
/**
* Returns the declaring class of the getter at the given index.
*
* @param index The index of the property for which to get the declaring class.
* @return The declaring class at the given index, or {@code null} if the index is out of bounds.
*
* @since 3.05
*/
final Class> getDeclaringClass(final int index) {
if (index >= 0 && index < names.length) {
return getters[index].getDeclaringClass();
}
return null;
}
/**
* Returns the name of the property at the given index, or {@code null} if none.
*
* @param index The index of the property for which to get the name.
* @param keyName The kind of name to return.
* @return The name of the given kind at the given index, or {@code null} if the
* index is out of bounds.
*/
@SuppressWarnings("fallthrough")
final String name(final int index, final KeyNamePolicy keyName) {
if (index >= 0 && index < names.length) {
switch (keyName) {
case UML_IDENTIFIER: {
final UML uml = getters[index].getAnnotation(UML.class);
if (uml != null) {
return uml.identifier();
}
// Fallthrough
}
case JAVABEANS_PROPERTY: {
return names[index];
}
case METHOD_NAME: {
return getters[index].getName();
}
case SENTENCE: {
return Strings.camelCaseToSentence(names[index]);
}
}
}
return null;
}
/**
* Returns the type of the property at the given index. The returned type is usually
* a GeoAPI interface (at least in the case of Geotk implementation). Primitive
* types like {@code double} or {@code int} are converted to their wrapper types.
*
* If the property is a collection, then this method returns the type of collection
* elements.
*
* @param index The index of the property.
* @param policy The kind of type to return.
* @return The type of property values, or {@code null} if unknown.
*/
final Class> type(final int index, final TypeValuePolicy policy) {
if (index >= 0 && index < standardCount) {
switch (policy) {
case ELEMENT_TYPE: {
return elementTypes[index];
}
case PROPERTY_TYPE: {
return getters[index].getReturnType();
}
case DECLARING_INTERFACE: {
return getters[index].getDeclaringClass();
}
case DECLARING_CLASS: {
Method getter = getters[index];
if (implementation != type) try {
getter = implementation.getMethod(getter.getName(), (Class>[]) null);
} catch (NoSuchMethodException error) {
// Should never happen, since the implementation class
// implements the interface where the getter come from.
throw new AssertionError(error);
}
return getter.getDeclaringClass();
}
}
}
return null;
}
/**
* Returns the restriction for the property at the given index, or {@code null} if none.
* The restriction, if any, typically contains a {@code NumberRange} object. More types
* may be added in future versions.
*/
final synchronized ValueRestriction restriction(final int index) {
ValueRestriction[] restrictions = this.restrictions;
if (restrictions == null) {
this.restrictions = restrictions = new ValueRestriction[standardCount];
Arrays.fill(restrictions, ValueRestriction.PENDING);
}
if (index < 0 || index >= restrictions.length) {
return null;
}
ValueRestriction restriction = restrictions[index];
if (restriction == ValueRestriction.PENDING) {
final Method impl, getter=getters[index];
if (implementation == type) {
impl = getter;
} else try {
impl = implementation.getMethod(getter.getName(), (Class>[]) null);
} catch (NoSuchMethodException error) {
// Should never happen, since the implementation class
// implements the interface where the getter come from.
throw new AssertionError(error);
}
restriction = ValueRestriction.create(elementTypes[index], getter, impl);
restrictions[index] = restriction;
}
return restriction;
}
/**
* Returns {@code true} if the property at the given index is writable.
*/
final boolean isWritable(final int index) {
return (index >= 0) && (index < standardCount) && (setters != null) && (setters[index] != null);
}
/**
* Returns the value for the specified metadata, or {@code null} if none.
*/
final Object get(final int index, final Object metadata) {
return (index >= 0 && index < standardCount) ? get(getters[index], metadata) : null;
}
/**
* Gets a value from the specified metadata. We do not expect any checked exception to
* be thrown, since {@code org.opengis.metadata} do not declare any.
*
* @param method The method to use for the query.
* @param metadata The metadata object to query.
*/
private static Object get(final Method method, final Object metadata) {
assert (method.getReturnType() != Void.TYPE) : method;
try {
return method.invoke(metadata, (Object[]) null);
} catch (IllegalAccessException e) {
// Should never happen since 'getters' should contains only public methods.
throw new AssertionError(e);
} catch (InvocationTargetException e) {
final Throwable cause = e.getTargetException();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new UndeclaredThrowableException(cause);
}
}
/**
* Sets a value for the specified metadata and returns the old value if {@code getOld} is
* {@code true}. If the old value was a collection or a map, then this value is copied in
* a new collection or map before the new value is set, because the setter methods typically
* copy the new collection in their existing instance.
*
* @param index The index of the property to set.
* @param metadata The metadata object on which to set the value.
* @param value The new value.
* @param getOld {@code true} if this method should first fetches the old value.
* @return The old value, or {@code null} if {@code getOld} was {@code false}.
* @throws IllegalArgumentException if the specified property can't be set.
* @throws ClassCastException if the given value is not of the expected type.
*/
final Object set(final int index, final Object metadata, final Object value, final boolean getOld)
throws IllegalArgumentException, ClassCastException
{
String key;
if (index >= 0 && index < standardCount && setters != null) {
final Method getter = getters[index];
final Method setter = setters[index];
if (setter != null) {
Object old;
if (getOld) {
old = get(getter, metadata);
if (old instanceof Collection>) {
old = XCollections.copy((Collection>) old);
} else if (old instanceof Map,?>) {
old = XCollections.copy((Map,?>) old);
}
} else {
old = null;
}
final Object[] newValues = new Object[] {value};
converter = convert(getter, metadata, newValues, elementTypes[index], converter);
set(setter, metadata, newValues);
return old;
} else {
key = getter.getName();
key = key.substring(prefix(key).length());
}
} else {
key = String.valueOf(index);
}
throw new IllegalArgumentException(Errors.format(Errors.Keys.ILLEGAL_ARGUMENT_$1, key));
}
/**
* Sets a value for the specified metadata. This method does not attempt any conversion of
* argument values. Conversion of type, if needed, must have been applied before to call
* this method.
*
* The call to the setter method should not thrown any checked exception.
* However unchecked exceptions are allowed.
*
* @param setter The method to use for setting the new value.
* @param metadata The metadata object to query.
* @param newValues The argument to give to the method to be invoked.
*/
private static void set(final Method setter, final Object metadata, final Object[] newValues) {
try {
setter.invoke(metadata, newValues);
} catch (IllegalAccessException e) {
// Should never happen since 'setters' should contains only public methods.
throw new AssertionError(e);
} catch (InvocationTargetException e) {
final Throwable cause = e.getTargetException();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new UndeclaredThrowableException(cause);
}
}
/**
* Converts a value to the type required by a setter method.
*
* @param getter
* The method to use for fetching the previous value.
* @param metadata
* The metadata object to query.
* @param newValues
* The argument to convert. It must be an array of length 1.
* The content of this array will be modified in-place.
* @param elementType
* The type required by the setter method.
* @param converter
* The last converter used, or {@code null} if none. This converter is provided only
* as a hint and doesn't need to be accurate.
* @return
* The last converter used, or {@code null}.
* @throws ClassCastException
* if the element of the {@code arguments} array is not of the expected type.
*/
private static ObjectConverter,?> convert(final Method getter, final Object metadata,
final Object[] newValues, Class> elementType, ObjectConverter,?> converter)
throws ClassCastException
{
assert newValues.length == 1;
Object newValue = newValues[0];
if (newValue != null) {
Class> targetType = getter.getReturnType();
if (!Collection.class.isAssignableFrom(targetType)) {
/*
* We do not expect a collection. The provided argument should not be a
* collection neither. It should be some class convertible to targetType.
*
* If nevertheless the user provided a collection and this collection contains
* no more than 1 element, then as a convenience we will extract the singleton
* element and process it as if it had been directly provided in argument.
*/
if (newValue instanceof Collection>) {
final Iterator> it = ((Collection>) newValue).iterator();
if (!it.hasNext()) { // If empty, process like null argument.
newValues[0] = null;
return converter;
}
final Object next = it.next();
if (!it.hasNext()) { // Singleton
newValue = next;
}
// Other cases: let the collection unchanged. It is likely to
// cause an exception later. The message should be appropriate.
}
// Getter type (targetType) shall be the same than the setter type (elementType).
assert elementType == Numbers.primitiveToWrapper(targetType) : elementType;
targetType = elementType; // Ensure that we use primitive wrapper.
} else {
/*
* We expect a collection. Collections are handled in one of the two ways below:
*
* - If the user gives a collection, the user's collection replaces any
* previous one. The content of the previous collection is discarded.
*
* - If the user gives a single value, it will be added to the existing
* collection (if any). The previous values are not discarded. This
* allow for incremental filling of a property.
*
* The code below prepares an array of elements to be converted and wraps that
* array in a List (to be converted to a Set after this block if required). It
* is okay to convert the elements after the List creation since the list is a
* wrapper.
*/
final Collection> addTo;
final Object[] elements;
if (newValue instanceof Collection>) {
elements = ((Collection>) newValue).toArray();
newValue = Arrays.asList(elements); // Content will be converted later.
addTo = null;
} else {
elements = new Object[] {newValue};
newValue = addTo = (Collection>) get(getter, metadata);
if (addTo == null) {
// No previous collection. Create one.
newValue = Arrays.asList(elements);
} else if (addTo instanceof CheckedCollection>) {
// Get the explicitly-specified element type.
elementType = ((CheckedCollection>) addTo).getElementType();
}
}
if (elementType != null) {
converter = convert(elements, elementType, converter);
}
/*
* We now have objects of the appropriate type. If we have a singleton to be added
* in an existing collection, add it now. In that case the 'newValue' should refer
* to the 'addTo' collection. We rely on ModifiableMetadata.copyCollection(...)
* optimization for detecting that the new collection is the same instance than
* the old one so there is nothing to do. We could exit from the method, but let
* it continues in case the user override the 'setFoo(...)' method.
*/
if (addTo != null) {
/*
* Unsafe addition into a collection. In Geotk implementation, the
* collection is actually an instance of CheckedCollection, so the check
* will be performed at runtime. However other implementations could use
* unchecked collection. There is not much we can do.
*/
@SuppressWarnings("unchecked")
final Collection