/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2007-2008, Open Source Geospatial Foundation (OSGeo) * * 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.geotools.imageio.metadata; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.NoSuchElementException; import java.util.StringTokenizer; import java.util.logging.Logger; import javax.imageio.metadata.IIOMetadataNode; import org.geotools.resources.Classes; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * Base class for {@linkplain SpatioTemporalMetadata metadata} parsers. This * class provides convenience methods for encoding and decoding metadata * information. A metadata root {@linkplain Node node} is specified at * construction time, together with a path to the {@linkplain Element element} * of interest. Example of valid paths: * <p> * <ul> * <li>{@code "AbstractCoordinateReferenceSystem/Datum"}</li> * <li>{@code "AbstractCoordinateReferenceSystem/CoordinateSystem"}</li> * <li>{@code "GridGeometry/Envelope"}</li> * </ul> * <p> * In addition, some elements contains an arbitrary amount of childs. The path * to child elements can also be specified to the constructor. Examples (note * that the constructor expects paths relative to the parent; we show absolute * paths below for completness): * <p> * <ul> * <li>{@code "AbstractCoordinateReferenceSystem/CoordinateSystem/Axis"}</li> * <li>{@code "GridGeometry/Envelope/CoordinateValues"}</li> * <li>{@code "Bands/Band"}</li> * </ul> * * The {@code get} and {@code set} methods defined in this class will operate on * the <cite>selected</cite> {@linkplain Element element}, which may be either * the one specified at construction time, or one of its childs. The element can * be selected by {@link #selectParent} (the default) or {@link #selectChild}. * <p> * The example below creates an accessor for a node called * {@code "CoordinateSystem"} which is expected to have childs called * {@code "Axis"}: * * <blockquote> * * <pre> * MetadataAccessor accessor = new MetadataAccessor(metadata, * "AbstractCoordinateReferenceSystem/CoordinateSystem", "Axis"); * * accessor.selectParent(); * String csName = accessor.getString("name"); * * accessor.selectChild(0); * String firstAxisName = accessor.getString("name"); * </pre> * * </blockquote> * * @author Martin Desruisseaux * @author Daniele Romagnoli, GeoSolutions * @author Alessio Fabiani, GeoSolutions * * @source $URL: http://svn.osgeo.org/geotools/branches/2.7.x/build/maven/javadoc/../../../modules/unsupported/coverage-experiment/coverage-core/src/main/java/org/geotools/imageio/metadata/MetadataAccessor.java $ */ public class MetadataAccessor { private final static Logger LOGGER = Logger.getLogger(MetadataAccessor.class.toString()); /** * The separator between names in a node path. */ protected static final char SEPARATOR = '/'; /** * The owner of this accessor. */ private final SpatioTemporalMetadata metadata; /** * The parent of child {@linkplain Element elements}. */ private final Node parent; /** * The {@linkplain #childs} path. This is the {@code childPath} parameter * given to the constructor. */ private final String childPath; /** * The list of child elements. May be empty but never null. */ private final List<Node> childs; /** * The current element, or {@code null} if not yet selected. * * @see #selectChild * @see #currentElement() */ private transient Element current; /** * Creates an accessor with the same parent and childs than the specified * one. The two accessors will share the same * {@linkplain Node metadata nodes} (including the list of childs), so * change in one accessor will be immediately reflected in the other * accessor. However each accessor can * {@linkplain #selectChild select their child} independently. * <p> * The main purpose of this constructor is to create many views over the * same list of childs, where each view {@linkplain #selectChild select} a * different child. */ protected MetadataAccessor(final MetadataAccessor clone) { metadata = clone.metadata; parent = clone.parent; childPath = clone.childPath; childs = clone.childs; } /** * Creates an accessor for the {@linkplain Element element} at the given * path. Paths are separated by the {@code '/'} character. See * {@linkplain MetadataAccessor class javadoc} for path examples. * * @param metadata * The metadata node. * @param parentPath * The path to the {@linkplain Node node} of interest, or * {@code null} if the {@code metadata} root node is directly * the node of interest. * @param childPath * The path (relative to {@code parentPath}) to the child * {@linkplain Element elements}, or {@code null} if none. */ protected MetadataAccessor(final SpatioTemporalMetadata metadata, final String parentPath, final String childPath) { this.metadata = metadata; final Node root = metadata.getRootNode(); /* * Fetches the parent node and ensure that we got a singleton. If there * is more nodes than expected, log a warning and pickup the first one. * If there is no node, create a new one. */ final List<Node> childs = new ArrayList<Node>(4); if (parentPath != null) { listChilds(root, parentPath, 0, childs, true); final int count = childs.size(); switch (count) { default: { // TODO: Handle this // warning("<init>", ErrorKeys.TOO_MANY_OCCURENCES_$2, // new Object[] {parentPath, count}); // Fall through for picking the first node. } case 1: { parent = childs.get(0); childs.clear(); break; } case 0: { parent = appendChild(root, parentPath); break; } } } else { parent = root; } /* * Computes a full path to children. Searching from 'metadata' root node * using 'path' should be identical to searching from 'parent' node * using 'childPath', except in case of badly formed metadata where the * parent node appears more than once. */ this.childPath = childPath; if (childPath != null) { final String path; if (parentPath != null) { path = parentPath + SEPARATOR + childPath; } else { path = childPath; } listChilds(root, path, 0, childs, false); this.childs = childs; } else { this.childs = Collections.emptyList(); } if (parent instanceof Element) { current = (Element) parent; } } protected MetadataAccessor(final MetadataAccessor parentNode, final String parentPath, final String childPath) { this.metadata = parentNode.metadata; final Node root = metadata.getRootNode(); /* * Fetches the parent node and ensure that we got a singleton. If there * is more nodes than expected, log a warning and pickup the first one. * If there is no node, create a new one. */ final List<Node> childs = new ArrayList<Node>(4); if (parentPath != null) { listChilds(root, parentPath, 0, childs, true); final int count = childs.size(); switch (count) { default: { // warning("<init>", ErrorKeys.TOO_MANY_OCCURENCES_$2, // new Object[] {parentPath, count}); // Fall through for picking the first node. } case 1: { parent = childs.get(0); childs.clear(); break; } case 0: { parent = appendChild(parentNode.current, parentPath); break; } } } else { parent = root; } /* * Computes a full path to children. Searching from 'metadata' root node * using 'path' should be identical to searching from 'parent' node * using 'childPath', except in case of badly formed metadata where the * parent node appears more than once. */ this.childPath = childPath; if (childPath != null) { final String path; if (parentPath != null) { path = parentPath + SEPARATOR + childPath; } else { path = childPath; } listChilds(root, path, 0, childs, false); this.childs = childs; } else { this.childs = Collections.emptyList(); } if (parent instanceof Element) { current = (Element) parent; } } /** * Adds to the {@link #childs} list the child nodes at the given * {@code path}. This method is for constructor implementation only and * invokes itself recursively. * * @param parent * The parent metadata node. * @param path * The path to the nodes or elements to insert into the list. * @param base * The offset in {@code path} for the next element name. * @param childs * The list where to insert the nodes or elements. * @param includeNodes * {@code true} of adding nodes as well as elements. */ private static void listChilds(final Node parent, final String path, final int base, final List<Node> childs, final boolean includeNodes) { final int upper = path.indexOf(SEPARATOR, base); final String name = ((upper >= 0) ? path.substring(base, upper) : path.substring(base)).trim(); final NodeList list = parent.getChildNodes(); final int length = list.getLength(); for (int i = 0; i < length; i++) { final Node candidate = list.item(i); if (name.equals(candidate.getNodeName())) { if (upper >= 0) { listChilds(candidate, path, upper + 1, childs, includeNodes); } else if (includeNodes || (candidate instanceof Element)) { // For the very last node, we may require an element. childs.add(candidate); } } } } /** * Appends a child to the given parent. * * @param parent * The parent to add a child to. * @param path * The path of the child to add. * @return element The new child. */ private static Node appendChild(Node parent, final String path) { int lower = 0; search: for (int upper; (upper = path.indexOf(SEPARATOR, lower)) >= 0; lower = upper + 1) { final String name = path.substring(lower, upper).trim(); final NodeList list = parent.getChildNodes(); final int length = list.getLength(); for (int i = length; --i >= 0;) { final Node candidate = list.item(i); if (name.equals(candidate.getNodeName())) { parent = candidate; continue search; } } parent = parent.appendChild(new IIOMetadataNode(name.intern())); } final String name = path.substring(lower).trim().intern(); return parent.appendChild(new IIOMetadataNode(name)); } /** * Returns the number of child {@linkplain Element elements}. This is the * upper value (exclusive) for {@link #selectChild}. * * @return The child {@linkplain Element elements} count. * * @see #selectChild * @see #appendChild */ protected int childCount() { return childs.size(); } /** * Adds a new child {@linkplain Element element} at the path given at * construction time. The {@linkplain #childCount child count} will be * increased by 1. * <p> * The new child is <strong>not</strong> automatically selected. In order * to select this new child, the {@link #selectChild} method must be invoked * explicitly. * * @return The index of the new child element. * * @see #childCount * @see #selectChild */ protected int appendChild() { final int size = childs.size(); final Node child = appendChild(parent, childPath); if (child instanceof Element) { childs.add((Element) child); return size; } else { throw new UnsupportedOperationException(child.getClass().toString()); } } /** * Selects the {@linkplain Element element} at the given index. Every * subsequent calls to {@code get} or {@code set} methods will apply to this * selected child element. * * @param index * The index of the element to select. * @throws IndexOutOfBoundsException * if the specified index is out of bounds. * * @see #childCount * @see #appendChild * @see #selectParent */ protected void selectChild(final int index) throws IndexOutOfBoundsException { current = (Element) childs.get(index); } /** * Selects the <em>parent</em> of child elements. Every subsequent calls * to {@code get} or {@code set} methods will apply to this parent element. * * @throws NoSuchElementException * if there is no parent {@linkplain Element element}. * * @see #selectChild */ protected void selectParent() throws NoSuchElementException { if (parent instanceof Element) { current = (Element) parent; } else { throw new NoSuchElementException(); } } /** * Returns the current element. * * @return The currently selected element. * @throws IllegalStateException * if there is no selected element. * * @see #selectChild */ private Element currentElement() throws IllegalStateException { if (current == null) { throw new IllegalStateException(); } return current; } /** * Returns the {@linkplain IIOMetadataNode#getUserObject user object} * associated with the {@linkplain #selectChild selected element}, or * {@code null} if none. If no user object is defined for the element, then * the {@linkplain Node#getNodeValue node value} is returned as a fallback. * This is consistent with {@link #setUserObject} implementation, and allows * some parsing of nodes that are not {@link IIOMetadataNode} instances. * <p> * The {@code getUserObject} methods are the only ones to not parse the * value returned by {@link #getString}. * * @return The user object, or {@code null} if none. * * @see #getUserObject(Class) * @see #setUserObject */ protected Object getUserObject() { final Element element = currentElement(); if (element instanceof IIOMetadataNode) { final Object candidate = ((IIOMetadataNode) element).getUserObject(); if (candidate != null) { return candidate; } } /* * getNodeValue() returns a String. We use it as a fallback, but in * typical IIOMetadataNode usage this value is not used (according its * javadoc), so it will often be null. */ return element.getNodeValue(); } /** * Returns the user object associated as an instance of the specified class. * If the value returned by {@link #getUserObject()} is not of the expected * type, then this method will tries to parse it as a string. * * @param type * The expected class. * @return The user object, or {@code null} if none. * @throws ClassCastException * if the user object can not be casted to the specified * type. * * @see #getUserObject() * @see #setUserObject */ protected <T> T getUserObject(Class<? extends T> type) throws ClassCastException { type = Classes.primitiveToWrapper(type).asSubclass(type); Object value = getUserObject(); if (value instanceof CharSequence) { if (Number.class.isAssignableFrom(type)) { value = Classes.valueOf(type, value.toString()); } else { final Class<?> component = Classes.primitiveToWrapper(type.getComponentType()); if (Double.class.equals(component)) { value = parseSequence(value.toString(), false, false); } else if (Integer.class.equals(component)) { value = parseSequence(value.toString(), false, true); } } } return type.cast(value); } /** * Sets the {@linkplain IIOMetadataNode#setUserObject user object} * associated with the {@linkplain #selectChild selected element}. This is * the only {@code set} method that doesn't invoke {@link #setString} with a * formatted value. * <p> * If the specified value is formattable (i.e. is a * {@linkplain CharSequence character sequence}, a * {@linkplain Number number} or an array of the above), then this method * also {@linkplain IIOMetadataNode#setNodeValue sets the node value} as a * string. This is mostly a convenience for formatting purpose since * {@link IIOMetadataNode} don't use the node value. But it may help some * libraries that are not designed to work with with user objects, since * they are particular to Image I/O metadata. * * @param value * The user object, or {@code null} if none. * @throws UnsupportedOperationException * if the selected element is not an instance of * {@link IIOMetadataNode}. * * @see #getUserObject() */ protected void setUserObject(final Object value) throws UnsupportedOperationException { final Element element = currentElement(); final String asText; if (isFormattable(value)) { asText = value.toString(); } else if (value != null && isFormattable(value.getClass().getComponentType())) { asText = formatSequence(value); } else { asText = null; } if (element instanceof IIOMetadataNode) { ((IIOMetadataNode) element).setUserObject(value); } else if (value != null && asText == null) { throw new UnsupportedOperationException("Illegal object. It should be an IIOMetadataNode instance"); } element.setNodeValue(asText); } /** * Returns {@code true} if the specified value can be formatted as a text. * We allows formatting only for reasonably cheap objects, for example a * Number but not a AbstractCoordinateReferenceSystem. */ private static boolean isFormattable(final Object value) { return (value instanceof CharSequence) || (value instanceof Number); } /** * Returns an attribute as a string for the * {@linkplain #selectChild selected element}, or {@code null} if none. * This method never returns an empty string. * <p> * Every {@code get} methods in this class except * {@link #getUserObject getUserObject} invoke this method first. * Consequently, this method provides a single point for overriding if * subclasses want to process the attribute before parsing. * * @param attribute * The attribute to fetch (e.g. {@code "name"}). * @return The attribute value (never an empty string), or {@code null} if * none. */ protected String getString(final String attribute) { String candidate = currentElement().getAttribute(attribute); if (candidate != null) { candidate = candidate.trim(); if (candidate.length() == 0) { candidate = null; } } return candidate; } /** * Set the attribute to the specified value, or remove the attribute if the * value is null. * <p> * Every {@code set} methods in this class except * {@link #setUserObject setUserObject} invoke this method last. * Consequently, this method provides a single point for overriding if * subclasses want to process the attribute after formatting. * * @param attribute * The attribute name. * @param value * The attribute value. */ protected void setString(final String attribute, String value) { final Element element = currentElement(); if (value == null || (value = value.trim()).length() == 0) { if (element.hasAttribute(attribute)) { element.removeAttribute(attribute); } } else { element.setAttribute(attribute, value); } } /** * Set the attribute to the specified enumeration value, or remove the * attribute if the value is null. * * @param attribute * The attribute name. * @param value * The attribute value. * @param enums * The set of allowed values, or {@code null} if unknown. */ final void setEnum(final String attribute, String value, final Collection enums) { if (value != null) { value = value.replace('_', ' ').trim(); for (final Iterator it = enums.iterator(); it.hasNext();) { final String e = (String) it.next(); if (value.equalsIgnoreCase(e)) { value = e; break; } } } setString(attribute, value); } /** * Returns an attribute as an integer for the * {@linkplain #selectChild selected element}, or {@code null} if none. If * the attribute can't be parsed as an integer, then this method logs a * warning and returns {@code null}. * * @param attribute * The attribute to fetch (e.g. {@code "minimum"}). * @return The attribute value, or {@code null} if none or unparseable. */ protected Integer getInteger(final String attribute) { String value = getString(attribute); if (value != null) { value = trimFractionalPart(value); try { return Integer.valueOf(value); } catch (NumberFormatException e) { // TODO: Handle this // warning("getInteger", ErrorKeys.UNPARSABLE_NUMBER_$1, value); } } return null; } /** * Set the attribute to the specified integer value. * * @param attribute * The attribute name. * @param value * The attribute value. */ protected void setInteger(final String attribute, final int value) { setString(attribute, Integer.toString(value)); } /** * Returns an attribute as an array of integers for the * {@linkplain #selectChild selected element}, or {@code null} if none. If * an element can't be parsed as an integer, then this method logs a warning * and returns {@code null}. * * @param attribute * The attribute to fetch (e.g. {@code "minimum"}). * @param unique * {@code true} if duplicated values should be collapsed into * unique values, or {@code false} for preserving duplicated * values. * @return The attribute values, or {@code null} if none. */ protected int[] getIntegers(final String attribute, final boolean unique) { return (int[]) parseSequence(getString(attribute), unique, true); } /** * Set the attribute to the specified array of values, or remove the * attribute if the array is {@code null}. * * @param attribute * The attribute name. * @param value * The attribute value. */ protected void setIntegers(final String attribute, final int[] values) { setString(attribute, formatSequence(values)); } /** * Returns an attribute as a floating point for the * {@linkplain #selectChild selected element}, or {@code null} if none. If * the attribute can't be parsed as a floating point, then this method logs * a warning and returns {@code null}. * * @param attribute * The attribute to fetch (e.g. {@code "minimum"}). * @return The attribute value, or {@code null} if none or unparseable. */ protected Double getDouble(final String attribute) { final String value = getString(attribute); if (value != null) try { return Double.valueOf(value); } catch (NumberFormatException e) { // TODO: Handle this // warning("getDouble", ErrorKeys.UNPARSABLE_NUMBER_$1, value); } return null; } /** * Set the attribute to the specified floating point value, or remove the * attribute if the value is NaN. * * @param attribute * The attribute name. * @param value * The attribute value. */ protected void setDouble(final String attribute, final double value) { String text = null; if (!Double.isNaN(value) && !Double.isInfinite(value)) { text = Double.toString(value); } setString(attribute, text); } /** * Returns an attribute as an array of floating point for the * {@linkplain #selectChild selected element}, or {@code null} if none. If * an element can't be parsed as a floating point, then this method logs a * warning and returns {@code null}. * * @param attribute * The attribute to fetch (e.g. {@code "fillValues"}). * @param unique * {@code true} if duplicated values should be collapsed into * unique values, or {@code false} for preserving duplicated * values. * @return The attribute values, or {@code null} if none. */ protected double[] getDoubles(final String attribute, final boolean unique) { return (double[]) parseSequence(getString(attribute), unique, false); } /** * Set the attribute to the specified array of values, or remove the * attribute if the array is {@code null}. * * @param attribute * The attribute name. * @param value * The attribute value. */ protected void setDoubles(final String attribute, final double[] values) { setString(attribute, formatSequence(values)); } /** * Implementation of {@link #getIntegers} and {@link #getDoubles} methods. * * @param sequence * The sequence to parse. * @param unique * {@code true} if duplicated values should be collapsed into * unique values, or {@code false} for preserving duplicated * values. * @param integers * {@code true} for parsing as {@code int}, or {@code false} * for parsing as {@code double}. * @return The attribute values, or {@code null} if none. */ private Object parseSequence(final String sequence, final boolean unique, final boolean integers) { if (sequence == null) { return null; } final Collection<Number> numbers; if (unique) { numbers = new LinkedHashSet<Number>(); } else { numbers = new ArrayList<Number>(); } final StringTokenizer tokens = new StringTokenizer(sequence); while (tokens.hasMoreTokens()) { final String token = tokens.nextToken(); final Number number; try { if (integers) { number = Integer.valueOf(token); } else { number = Double.valueOf(token); } } catch (NumberFormatException e) { // TODO: Handle this // warning(integers ? "getIntegers" : "getDoubles", // ErrorKeys.UNPARSABLE_NUMBER_$1, token); continue; } numbers.add(number); } int count = 0; final Object values; if (integers) { values = new int[numbers.size()]; } else { values = new double[numbers.size()]; } for (final Iterator<Number> it = numbers.iterator(); it.hasNext();) { Array.set(values, count++, it.next()); } assert Array.getLength(values) == count; return values; } /** * Formats a sequence for {@link #setIntegers} and {@link #setDoubles} * implementations. * * @param value * The attribute value. * @return The formatted sequence. */ private static String formatSequence(final Object values) { String text = null; if (values != null) { final StringBuilder buffer = new StringBuilder(); final int length = Array.getLength(values); for (int i = 0; i < length; i++) { if (i != 0) { buffer.append(' '); } buffer.append(Array.get(values, i)); } text = buffer.toString(); } return text; } /** * Trims the fractional part of the given string, provided that it doesn't * change the value. More specifically, this method removes the trailing * {@code ".0"} characters if any. This method is automatically invoked * before to {@linkplain #getInteger parse an integer} or to * {@linkplain #getDate parse a date} (for simplifying fractional seconds). * * @param value * The value to trim. * @return The value without the trailing {@code ".0"} part. */ public static String trimFractionalPart(String value) { value = value.trim(); for (int i = value.length(); --i >= 0;) { switch (value.charAt(i)) { case '0': continue; case '.': return value.substring(0, i); default: return value; } } return value; } }