/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2009-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2009-2012, 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.image.io.metadata; import java.util.Map; import java.util.Date; import java.util.List; import java.util.HashMap; import java.util.Collection; import java.util.Collections; import java.util.logging.Level; import java.util.NoSuchElementException; import java.lang.reflect.Proxy; import java.lang.reflect.Method; import java.lang.reflect.InvocationHandler; import javax.imageio.metadata.IIOMetadataFormat; import javax.measure.Quantity; import javax.measure.Unit; import org.opengis.util.CodeList; import org.opengis.util.InternationalString; import org.opengis.metadata.citation.Citation; import org.opengis.coverage.grid.GridEnvelope; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.Classes; import org.apache.sis.util.iso.Types; import org.apache.sis.internal.util.UnmodifiableArrayList; import org.geotoolkit.coverage.grid.GeneralGridCoordinates; import org.apache.sis.geometry.GeneralDirectPosition; import org.geotoolkit.resources.Errors; /** * Implementation of metadata interfaces. Calls to getter methods are converted into calls to the * metadata accessor extracting an attribute from the {@link javax.imageio.metadata.IIOMetadata} * object. * * {@section Naming conventions for elements and attributes} * This class assumes that the elements and attributes are named according the same rules than * the ones used by the {@code SpatialMetadataFormat.addTree(...)} methods. More specifically: * * <ul> * <li><p>Attribute names are inferred from the method names according the * {@link SpatialMetadataFormat#NAME_POLICY}, which follow Java-Beans conventions.</p></li> * * <li><p>Element names are inferred in the same way than attribute names (see above), * except that the first character is converted to an upper-case character using * the {@link SpatialMetadataFormat#toElementName(String)} method.</p></li> * * <li><p><b>Special case for lists:</b> if an element is not found, then this class looks * for an element having a name in the singular form, where the name is inferred from * the UML identifier and {@link SpatialMetadataFormat#toComponentName}. The intend is * to support the case where the XML tree has been simplified with the replacement of * collections by singletons.</p></li> * </ul> * * @param <T> The metadata interface implemented by the proxy. * * @author Martin Desruisseaux (Geomatys) * @version 3.19 * * @since 3.06 * @module */ final class MetadataProxy<T> implements InvocationHandler { /** * {@code true} for enabling the process of a few Geotk-specific special cases. This field * should always be {@code true}. It is defined mostly as a way to spot every places where * some special cases are defined. * * @see #getAttributeLength(String) * @see #getAttributeAsInteger(String, int) */ private static final boolean SPECIAL_CASE = true; /** * The interface implemented by the proxy. */ final Class<T> interfaceType; /** * The metadata accessor. This is used for fetching the value of an attribute. The name of * the attribute is inferred from the method name using the {@linkplain #namesMapping} map. */ final MetadataNodeParser accessor; /** * The index of the child element, or -1 if none. */ private final int index; /** * The mapping from method names to attribute names, or {@code null} if this mapping * is unknown. Keys are method names, and values are the attribute name as determined * by {@link SpatialMetadataFormat#NAME_POLICY}. */ private final Map<String, String> namesMapping; /** * The children created up to date. This is used only when the return type of some * invoked methods is a {@link java.util.Collection}, {@link java.util.List} or an * other metadata interface. * <p> * The keys are method names (instead than attribute names) because they are * usually internalized by the JVM, which is not the case of the attribute names. */ private transient Map<String, Object> childs; /** * Creates a new proxy having the same properties than the given one except for the index. * This is used for creating elements in a list. */ private MetadataProxy(final MetadataProxy<T> prototype, final int index) { interfaceType = prototype.interfaceType; namesMapping = prototype.namesMapping; accessor = prototype.accessor; this.index = index; } /** * Creates a new proxy for the given metadata accessor. */ MetadataProxy(final Class<T> type, final MetadataNodeParser accessor) { interfaceType = type; this.accessor = accessor; this.index = -1; final IIOMetadataFormat format = accessor.format; if (format instanceof SpatialMetadataFormat) { namesMapping = ((SpatialMetadataFormat) format).getElementNames(accessor.name()); } else { namesMapping = null; } } /** * Returns a new instance of a proxy class for the specified metadata interface. * * @param type The interface for which to create a proxy instance. * @param accessor The metadata accessor. * @throws IllegalArgumentException If the given type is not a valid interface * (see {@link Proxy} javadoc for a list of the conditions). */ static <T> T newProxyInstance(final Class<T> type, final MetadataNodeParser accessor) { return type.cast(Proxy.newProxyInstance(MetadataProxy.class.getClassLoader(), new Class<?>[] {type}, new MetadataProxy<>(type, accessor))); } /** * Returns a new instance of a proxy class with the same properties than this instance * but a different index. This is used for creating elements in a list. */ final T newProxyInstance(final int index) { final Class<T> type = interfaceType; return type.cast(Proxy.newProxyInstance(MetadataProxy.class.getClassLoader(), new Class<?>[] {type}, new MetadataProxy<>(this, index))); } /** * Returns the attribute name for the given method name. The caller must have verified * that the method name starts with either {@code "get"}, {@code "set"} or {@code "is"}. * * @param methodName The value of {@link Method#getName()}. * @return The name of the attribute to search in the {@code IIOMetadataNode} * wrapped by the {@linkplain #accessor}. */ @SuppressWarnings("fallthrough") private String getAttributeName(final String methodName) { if (namesMapping != null) { final String attribute = namesMapping.get(methodName); if (attribute != null) { return attribute; } } /* * If no mapping is explicitly declared for the given method name, apply JavaBeans * conventions. If the prefix is not "is", the code below assumes "get" or "set". */ final int offset = methodName.startsWith("is") ? 2 : 3; // Prefix length final int length = methodName.length(); switch (length - offset) { default: { /* * If there is at least 2 characters after the prefix, assume that * we have an acronym if the two first character are upper case. */ if (Character.isUpperCase(methodName.charAt(offset)) && Character.isUpperCase(methodName.charAt(offset+1))) { return methodName.substring(offset); } // Fall through } case 1: { /* * If we have at least one character, make the first character lower-case. */ return new StringBuilder(length - offset) .append(Character.toLowerCase(methodName.charAt(offset))) .append(methodName, offset+1, length).toString(); } case 0: { /* * If we have only the prefix, return it unchanged. */ return methodName; } } } /** * Returns the type of user object for the given element. This typically equals to the * {@linkplain Method#getReturnType() method return type}, but is some occasion the * {@link IIOMetadataFormat} forces a sub-type. * * @param name The element name. * @param methodType The type inferred from the method signature, or {@code null} if unknown. * @return The type to use, which is guaranteed to be assignable to the method type. * @throws IllegalArgumentException If the named element does not exist or does not define objects. */ private Class<?> getElementClass(final String name, final Class<?> methodType) throws IllegalArgumentException { final IIOMetadataFormat format = accessor.format; final int valueType = format.getObjectValueType(name); if (valueType != IIOMetadataFormat.VALUE_NONE) { Class<?> declaredType = format.getObjectClass(name); // Not allowed to be null. if (valueType == IIOMetadataFormat.VALUE_LIST) { declaredType = Classes.changeArrayDimension(declaredType, 1); } if (methodType == null || methodType.isAssignableFrom(declaredType)) { return declaredType; } } return methodType; } /** * Casts the {@code type} class to represent a subclass of the class represented by the * {@code sub} argument. Checks that the cast is valid, and returns {@code null} if it * is not. * <p> * This method performs the same work than * <code>type.{@linkplain Class#asSubclass(Class) asSubclass}(sub)</code>, * except that {@code null} is returned instead than throwing an exception * if the cast is not valid or if any of the argument is {@code null}. * * @param <U> The compile-time bounds of the {@code sub} argument. * @param type The class to cast to a sub-class, or {@code null}. * @param sub The subclass to cast to, or {@code null}. * @return The {@code type} argument casted to a subclass of the {@code sub} argument, * or {@code null} if this cast can not be performed. * * @see Class#asSubclass(Class) */ @SuppressWarnings("unchecked") private static <U> Class<? extends U> asSubclassOrNull(final Class<?> type, final Class<U> sub) { // Design note: We are required to return null if 'sub' is null (not to return 'type' // unchanged), because if we returned 'type', we would have an unsafe cast if this // method is invoked indirectly from a parameterized method. return (type != null && sub != null && sub.isAssignableFrom(type)) ? (Class) type : null; } /** * For an attribute containing an array, return the length of that array. * This is used only for implementing a few {@link #SPECIAL_CASE special cases}. */ private int getAttributeLength(final String name) { final int[] values = accessor.getAttributeAsIntegers(name, false); return (values != null) ? values.length : 0; } /** * For an attribute containing an array, return the attribute at the given index from that * array. This is used only for implementing a few {@link #SPECIAL_CASE special cases}. */ private int getAttributeAsInteger(final String name, final int index) { final int[] values = accessor.getAttributeAsIntegers(name, false); return (values != null) ? values[index] : 0; } /** * Invoked when a method from the metadata interface has been invoked. * * @param proxy The proxy instance that the method was invoked on. * @param method The method from the interface which have been invoked. * @param args The arguments, or {@code null} if the method takes no argument. * @return The value to return from the method invocation on the proxy instance. * @throws UnsupportedOperationException If the given method is not supported. * @throws IllegalArgumentException If {@code args} contains a value while none was expected. * @throws IllegalStateException If the attribute value can not be converted to the return type. */ @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws UnsupportedOperationException, IllegalArgumentException, IllegalStateException { /* * We accept only calls to getter methods, except for a few non-final * methods defined in Object that we need to define for compliance. */ final String methodName = method.getName(); final int numArgs = (args != null) ? args.length : 0; if (!methodName.startsWith("get") && !methodName.startsWith("is")) { switch (numArgs) { case 0: { if (methodName.equals("toString")) { return toProxyString(); } if (methodName.equals("hashCode")) { return System.identityHashCode(proxy); } break; } case 1: { if (methodName.equals("equals")) { return proxy == args[0]; } break; } } throw new UnsupportedOperationException(Errors.format( Errors.Keys.UnknownCommand_1, methodName)); } /* * If an argument is provided to the method, this is an error since we don't know * how to handle that, except for a few hard-coded special cases. */ if (numArgs != 0) { if (SPECIAL_CASE && numArgs == 1) { final Object arg = args[0]; if (arg instanceof Integer) { final int dim = (Integer) arg; if (proxy instanceof GridEnvelope) { switch (methodName) { case "getLow": return getAttributeAsInteger("low", dim); case "getHigh": return getAttributeAsInteger("high", dim); case "getSpan": return getAttributeAsInteger("high", dim) - getAttributeAsInteger("low", dim) + 1; } } } } throw new IllegalArgumentException(Errors.format( Errors.Keys.UnexpectedArgumentForInstruction_1, methodName)); } /* * Gets the name of the attribute to fetch, and set the accessor * child index on the children represented by this proxy (if any). */ final MetadataNodeParser accessor = this.accessor; final String name = getAttributeName(methodName); if (index >= 0) { accessor.selectChild(index); } else { accessor.selectParent(); } /* * First, process the cases that are handled in a special way. The order is significant: * if the target type is some generic type like java.lang.Object, then we want to select * the method performing the less transformation (String if the target type is Object, * Double rather than Integer if the target type is Number). */ final Class<?> targetType = method.getReturnType(); if (targetType.equals(Double.TYPE)) { Double value = accessor.getAttributeAsDouble(name); if (value == null) value = Double.NaN; return value; } if (targetType.equals(Float.TYPE)) { Float value = accessor.getAttributeAsFloat(name); if (value == null) value = Float.NaN; return value; } if (targetType.equals(Long.TYPE)) { Integer value = accessor.getAttributeAsInteger(name); return Long.valueOf((value != null) ? value.longValue() : 0L); } if (targetType.equals(Integer.TYPE)) { Integer value = accessor.getAttributeAsInteger(name); if (value == null) { value = 0; if (SPECIAL_CASE) { if (methodName.equals("getDimension") && proxy instanceof GridEnvelope) { value = Math.max(getAttributeLength("low"), getAttributeLength("high")); } } } return value; } if (targetType.equals(Short.TYPE)) { Integer value = accessor.getAttributeAsInteger(name); return Short.valueOf((value != null) ? value.shortValue() : (short) 0); } if (targetType.equals(Byte.TYPE)) { Integer value = accessor.getAttributeAsInteger(name); return Byte.valueOf((value != null) ? value.byteValue() : (byte) 0); } if (targetType.equals(Boolean.TYPE)) { Boolean value = accessor.getAttributeAsBoolean(name); if (value == null) value = Boolean.FALSE; return value; } if (targetType.isAssignableFrom(String .class)) return accessor.getAttribute (name); if (targetType.isAssignableFrom(Double .class)) return accessor.getAttributeAsDouble (name); if (targetType.isAssignableFrom(Float .class)) return accessor.getAttributeAsFloat (name); if (targetType.isAssignableFrom(Integer .class)) return accessor.getAttributeAsInteger (name); if (targetType.isAssignableFrom(Boolean .class)) return accessor.getAttributeAsBoolean (name); if (targetType.isAssignableFrom(String[] .class)) return accessor.getAttributeAsStrings (name, false); if (targetType.isAssignableFrom(double[] .class)) return accessor.getAttributeAsDoubles (name, false); if (targetType.isAssignableFrom(float[] .class)) return accessor.getAttributeAsFloats (name, false); if (targetType.isAssignableFrom(int[] .class)) return accessor.getAttributeAsIntegers(name, false); if (targetType.isAssignableFrom(Date .class)) return accessor.getAttributeAsDate (name); if (targetType.isAssignableFrom(NumberRange.class)) return accessor.getAttributeAsRange (name); if (targetType.isAssignableFrom(Citation .class)) return accessor.getAttributeAsCitation(name); if (targetType.isAssignableFrom(InternationalString.class)) { return Types.toInternationalString(accessor.getAttribute(name)); } if (targetType.isAssignableFrom(Unit.class)) { final Class<?> bounds = Classes.boundOfParameterizedProperty(method); return accessor.getAttributeAsUnit(name, asSubclassOrNull(bounds, Quantity.class)); } if (SPECIAL_CASE && Character.isLowerCase(name.charAt(0))) { /* * We have not yet defined a good public API for those cases. For the time being, * we use the case of the node name in order to detect if we have an attribute * instead than an element. */ if (targetType.isAssignableFrom(GeneralDirectPosition.class)) { final double[] ordinates = accessor.getAttributeAsDoubles(name, false); return (ordinates != null) ? new GeneralDirectPosition(ordinates) : null; } if (targetType.isAssignableFrom(GeneralGridCoordinates.class)) { final int[] ordinates = accessor.getAttributeAsIntegers(name, false); return (ordinates != null) ? new GeneralGridCoordinates(ordinates) : null; } } if (targetType.isAssignableFrom(List.class)) { /* * The return type is a list or a collection. If the collection elements are some * simple types, then we assume that it still an attribute. We do not cache this * value because we want to recompute it on next method call (the returned list * is not "live"). */ Class<?> componentType = Classes.boundOfParameterizedProperty(method); if (componentType.isAssignableFrom(String.class)) { return UnmodifiableArrayList.wrap(accessor.getAttributeAsStrings(name, false)); } if (componentType.isAssignableFrom(InternationalString.class)) { return UnmodifiableArrayList.wrap(Types.toInternationalStrings( accessor.getAttributeAsStrings(name, false))); } if (componentType.isAssignableFrom(Citation.class)) { return Collections.singletonList(accessor.getAttributeAsCitation(name)); } /* * Assume a nested element. We will create a "live" list and cache it for future * reuse. The type of list elements may be a subtype of 'componentType' because * IIOMetadataFormat may specify restrictions. */ if (childs == null) { childs = new HashMap<>(); } List<?> list = (List<?>) childs.get(methodName); if (list == null) { String elementName = SpatialMetadataFormat.toElementName(name); final MetadataNodeParser acc; try { acc = new MetadataNodeParser(accessor, elementName, "#auto"); } catch (IllegalArgumentException e) { /* * This exception happen when no node for 'elementName' is defined in the * IIOMetadataFormat used by the accessor. For example, DiscoveryMetadata * node in stream SpatialMetadataFormat omits the 'languages' attribute. */ Logging.recoverableException(MetadataNodeParser.LOGGER, interfaceType, methodName, e); return null; } catch (NoSuchElementException e) { // There is no value for this node. return Collections.emptyList(); } /* * At this point we have a MetadataNodeParser to a node which is known to exist. * This node may have no children, in which case we need to wraps the singleton * in a list. */ if (acc.allowsChildren()) { componentType = getElementClass(acc.childPath, componentType); list = acc.newProxyList(componentType); } else { componentType = getElementClass(elementName, componentType); list = Collections.singletonList(acc.newProxyInstance(componentType)); } childs.put(methodName, list); } return list; } /* * Code lists case. Only existing instances are returned; no new instance is created. */ if (CodeList.class.isAssignableFrom(targetType)) { @SuppressWarnings({"unchecked","rawtypes"}) final CodeList code = accessor.getAttributeAsCode(name, (Class) targetType); return code; } if (Enum.class.isAssignableFrom(targetType)) { @SuppressWarnings({"unchecked","rawtypes"}) final Enum code = accessor.getAttributeAsEnum(name, (Class) targetType); return code; } /* * For all other types, assume a nested child element. * A new proxy will be created for the nested child. */ if (childs == null) { childs = new HashMap<>(); } Object child = childs.get(methodName); if (child == null) { try { // Each of the last 3 lines may throw, directly or indirectly, an IllegalArgumentException. final String elementName = SpatialMetadataFormat.toElementName(name); final Class<?> elementType = getElementClass(elementName, targetType); final MetadataNodeParser acc = new MetadataNodeParser(accessor, elementName, "#auto"); child = acc.isEmpty() ? Void.TYPE : acc.newProxyInstance(elementType); } catch (IllegalArgumentException e) { /* * Report the warning and remember that we can not return a value for this * element, so we don't try again next time. We use a lower warning level * since this exception can be considered normal (IIOMetadataFormat does * not define every attributes). */ accessor.warning(Level.FINE, interfaceType, methodName, e); child = Void.TYPE; } catch (NoSuchElementException e) { // There is no value for this node. child = Void.TYPE; } childs.put(methodName, child); } return (child != Void.TYPE) ? child : null; } /** * Sets the warning level of all proxy instances in the given collection. * * @param instances The collections of proxy instances. * @param level The new warning level. */ static void setWarningLevel(final Collection<?> instances, final Level level) { for (final Object proxy : instances) { if (proxy instanceof MetadataProxyList<?>) { ((MetadataProxyList<?>) proxy).setWarningLevel(level); } else if (proxy instanceof Collection<?>) { setWarningLevel((Collection<?>) proxy, level); } else if (proxy != null) { final MetadataProxy<?> handler = (MetadataProxy<?>) Proxy.getInvocationHandler(proxy); if (!level.equals(handler.accessor.setWarningLevel(level))) { // Recursive calls only if the level changed. This // is a precaution against infinite recursivity. final Map<String, Object> childs = handler.childs; if (childs != null) { setWarningLevel(childs.values(), level); } } } } } /** * Returns a string representation of the proxy. This is mostly for debugging * purpose and may change in any future version. */ private String toProxyString() { if (index >= 0) { return Classes.getShortName(interfaceType) + '[' + index + ']'; } return accessor.toString(interfaceType); } /** * Returns a string representation of the {@linkplain #accessor}, but declaring the * class as {@code MetadataProxy} instead than {@code MetadataNodeParser}. */ @Override public String toString() { return accessor.toString(getClass()); } }