/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2007-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.lang.reflect.Array; import java.util.Date; import java.util.NoSuchElementException; import javax.imageio.metadata.IIOMetadata; import javax.imageio.metadata.IIOMetadataNode; import javax.imageio.metadata.IIOMetadataFormat; import javax.measure.Unit; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.opengis.util.ControlledVocabulary; import org.opengis.metadata.citation.Citation; import org.geotoolkit.resources.Errors; import org.apache.sis.util.iso.Types; import org.geotoolkit.internal.jdk8.JDK8; import org.apache.sis.measure.NumberRange; import org.apache.sis.util.Classes; import org.apache.sis.util.Numbers; import org.apache.sis.util.UnsupportedImplementationException; import org.apache.sis.metadata.iso.citation.Citations; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; /** * Convenience class for reading and writing attribute values from/to an {@link IIOMetadata} object. * This class is used by {@link SpatialMetadata} and usually don't need to be created explicitly. * It is available in public API for users who need more flexibility than what * {@code SpatialMetadata} provides. * <p> * The metadata object is specified at construction time, together with a path to the * {@linkplain Element element} of interest. Examples of valid paths: * * <blockquote><table cellspacing="0" cellpadding="0"> * <tr> * <td>{@code "RectifiedGridDomain/CRS/Datum"}</td> * <td>  (assuming the {@linkplain SpatialMetadataFormat#getImageInstance image} metadata format)</td> * </tr><tr> * <td>{@code "RectifiedGridDomain/CRS/CoordinateSystem"}</td> * <td>  (assuming the {@linkplain SpatialMetadataFormat#getImageInstance image} metadata format)</td> * </tr><tr> * <td>{@code "DiscoveryMetadata/Extent/GeographicElement"}</td> * <td>  (assuming the {@linkplain SpatialMetadataFormat#getStreamInstance stream} metadata format)</td> * </tr> * </table></blockquote> * * If no node exists for the given path, then the node will be created at {@code MetadataNodeAccessor} * construction time. For example the last line in the above list will ensure that the metadata * tree contains at least the nodes below, creating the missing ones if needed: * * {@preformat text * <root> * └───DiscoveryMetadata * └───Extent * └───GeographicElement * } * * <blockquote><font size="-1"><b>Note:</b> the value of {@code <root>} depends on the metadata * format, but is typically * {@value org.geotoolkit.image.io.metadata.SpatialMetadataFormat#FORMAT_NAME}</font></blockquote> * * After a {@code MetadataNodeAccessor} instance has been created, the {@code getAttributeAs<Type>(String)} * methods can be invoked for fetching any attribute values, taking care of conversions to * {@link String}, {@link Double}, {@link Integer} or {@link Date}. Corresponding setter * methods are also provided. * * {@section Accessing child elements} * If order to access a child element when the child policy is * {@link IIOMetadataFormat#CHILD_POLICY_ALL CHILD_POLICY_ALL}, * {@link IIOMetadataFormat#CHILD_POLICY_SOME CHILD_POLICY_SOME} or * {@link IIOMetadataFormat#CHILD_POLICY_CHOICE CHILD_POLICY_CHOICE}, * create a new {@code MetadataNodeAccessor} with the complete path to that element. * <p> * If the child policy of the node is {@link IIOMetadataFormat#CHILD_POLICY_REPEAT CHILD_POLICY_REPEAT}, * then this class provides convenience methods for accessing the attributes of the childs. * The path to unique legal child elements shall be specified to the constructor, as in the * examples below: * <p> * <ul> * <li>{@code new MetadataNodeAccessor(..., "RectifiedGridDomain/CRS/CoordinateSystem", "Axis")}</li> * <li>{@code new MetadataNodeAccessor(..., "ImageDescription/Dimensions", "Dimension")}</li> * </ul> * <p> * 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(int)}. * <p> * Note that this mechanism is not suitable to nested childs, i.e. {@code MetadataNodeAccessor} gives * access only to the attributes of child elements. If an access to the childs nested in a child * element is wanted, then the users may find more convenient to parse the XML tree by an other * way than this convenience class. * * {@section Example reading attributes} * The example below creates an accessor for a node called {@code "CoordinateSystem"} * which is expected to have an arbitrary amount of childs called {@code "Axis"}. The * name of the first axis is fetched. * * {@preformat java * IIOMetadata metadata = new SpatialMetadata(SpatialMetadataFormat.IMAGE); * MetadataNodeAccessor accessor = new MetadataNodeAccessor(metadata, null, * "RectifiedGridDomain/CRS/CoordinateSystem", "Axis"); * * accessor.selectParent(); * String csName = accessor.getAttribute("name"); * * accessor.selectChild(0); * String firstAxisName = accessor.getAttribute("name"); * } * * {@section Example adding childs and writing attributes} * The example below uses the same accessor than above, but this time for adding a new * child under the {@code "CoordinateSystem"} node: * * {@preformat java * accessor.selectChild(accessor.appendChild()); * accessor.setAttribute("name", "The name of a new axis"); * } * * {@section Getting ISO 19115-2 instances} * This class can provide implementations of the ISO 19115-2 interfaces. Each getter method in * an interface is implemented as a call to a {@code getAttribute(String)} method, or as the * creation of a nested ISO 19115-2 object. See {@link #newProxyInstance(Class)} for more details. * <p> * While this mechanism is primarily targeted at ISO 19115-2 interfaces, it can be used with * other set of interfaces as well. * * @author Martin Desruisseaux (Geomatys) * @author Cédric Briançon (Geomatys) * @version 3.20 * * @see SpatialMetadata#getInstanceForType(Class) * @see SpatialMetadata#getListForType(Class) * * @since 3.20 (derived from 2.5) * @module */ public class MetadataNodeAccessor extends MetadataNodeParser { /** * 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(int) select their child} * independently. * <p> * The initially {@linkplain #selectChild(int) selected child} and {@linkplain #getWarningLevel() * warnings level} are the same than the given accessor. * <p> * The main purpose of this constructor is to create many views over the same list * of childs, where each view can {@linkplain #selectChild(int) select} a different child. * * @param clone The accessor to clone. */ public MetadataNodeAccessor(final MetadataNodeParser clone) { super(clone); } /** * Creates an accessor for the {@linkplain Element element} at the given path relative * to the given parent. In the example below, the complete path to the child accessor * is {@code "DiscoveryMetadata/Extent/GeographicElement"}: * * {@preformat java * MetadataNodeAccessor parent = new MetadataNodeAccessor(..., "DiscoveryMetadata/Extent", ...); * MetadataNodeAccessor child = new MetadataNodeAccessor(parent, "GeographicElement", null); * } * * {@section Auto-detection of children} * If the metadata node has no child, then {@code childPath} shall be {@code null}. * If the caller does not know whatever the node has childs or not, then the * {@code "#auto"} special value can be used. Note that this auto-detection may * throw an {@link IllegalArgumentException} if the node is not defined by the * {@link IIOMetadataFormat}. It is preferable to specify explicitly the child * element name when this name is known. * * @param parent The accessor for which the {@code path} is relative. * @param path The path to the {@linkplain Node node} of interest. * @param childPath The path to the child {@linkplain Element elements}, or {@code null} * if none, or {@code "#auto"} for auto-detection. * * @throws IllegalArgumentException if {@code childPath} is {@code "#auto"} but the childs * can not be inferred from the metadata format. * @throws NoSuchElementException If this accessor is {@linkplain #isReadOnly() is read only} * and the given metadata doesn't contains a node for the element to fetch. * * @since 3.06 */ public MetadataNodeAccessor(MetadataNodeParser parent, String path, String childPath) throws IllegalArgumentException, NoSuchElementException { super(parent, path, childPath); } /** * Creates an accessor for the {@linkplain Element element} accepting a user object of the * given type. This method is convenient for fetching an object of some known type without * regards to its location in the sub-tree. * * @param parent The accessor from which to start the search for an element accepting * the given type. * @param objectClass The {@linkplain IIOMetadataFormat#getObjectClass(String) class of user * object} to locate. * * @throws IllegalArgumentException If no element accepting the given type was found, * or if more than one element accepting that type was found. * @throws NoSuchElementException If this accessor is {@linkplain #isReadOnly() is read only} * and the given metadata doesn't contains a node for the element to fetch. * * @see #listPaths(IIOMetadataFormat, Class) * * @since 3.06 */ public MetadataNodeAccessor(MetadataNodeParser parent, Class<?> objectClass) throws IllegalArgumentException, NoSuchElementException { super(parent, objectClass); } /** * Creates an accessor for the {@linkplain Element element} at the given path relative to * the {@linkplain IIOMetadataFormat#getRootName() root}. This is a convenience method for the * {@linkplain #MetadataNodeAccessor(IIOMetadata, String, String, String) constructor below} * with {@code formatName} and {@code childPath} argument set to {@code "#auto"} value. * * @param metadata The Image I/O metadata. An instance of the {@link SpatialMetadata} * sub-class is recommended, but not mandatory. * @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. * * @throws NoSuchElementException If this accessor is {@linkplain #isReadOnly() is read only} * and the given metadata doesn't contains a node for the element to fetch. * * @since 3.06 */ public MetadataNodeAccessor(IIOMetadata metadata, String parentPath) throws NoSuchElementException { super(metadata, parentPath); } /** * Creates an accessor for the {@linkplain Element element} at the given path relative to * the {@linkplain IIOMetadataFormat#getRootName() root}. The paths can contain many elements * separated by the {@code '/'} character. * See the <a href="#skip-navbar_top">class javadoc</a> for more details. * * {@section Auto-detection of children and format} * The {@code childPath} argument can be {@code "#auto"}, which is processed as documented * in the {@linkplain #MetadataNodeAccessor(MetadataNodeParser, String, String) above constructor}. * <p> * The {@code formatName} can be {@code null} or {@code "#auto"}, in which case a format * is selected automatically: * <p> * <ul> * <li>If {@code metadata} is an instance of {@link SpatialMetadata}, then the * {@linkplain SpatialMetadata#format format} given at {@code SpatialMetadata} * construction time is used.</li> * <li>Otherwise the first format returned by {@link IIOMetadata#getMetadataFormatNames()} * is used. This is usually in preference order: the native format, the standard format * or the first extra format.</li> * </ul> * * @param metadata The Image I/O metadata. An instance of the {@link SpatialMetadata} * sub-class is recommended, but not mandatory. * @param formatName The name of the {@linkplain IIOMetadata#getMetadataFormat(String) format * to use}, or {@code null} or {@code "#auto"} for an automatic selection. * @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, * or {@code "#auto"} for auto-detection. * * @throws NoSuchElementException If this accessor is {@linkplain #isReadOnly() is read only} * and the given metadata doesn't contains a node for the element to fetch. */ public MetadataNodeAccessor(IIOMetadata metadata, String formatName, String parentPath, String childPath) throws NoSuchElementException { super(metadata, formatName, parentPath, childPath); } /** * Creates an accessor for the {@linkplain Element element} accepting a user object of the * given type. This method is convenient for fetching an object of some known type without * regards to its location in the tree. For example if the metadata format stream format * documented in {@link SpatialMetadataFormat}, then: * * {@preformat java * new MetadataNodeAccessor(metadata, formatName, GeographicElement.class); * } * * is equivalent to: * * {@preformat java * new MetadataNodeAccessor(metadata, formatName, "DiscoveryMetadata/Extent/GeographicElement", "#auto"); * } * * @param metadata The Image I/O metadata. An instance of the {@link SpatialMetadata} * sub-class is recommended, but not mandatory. * @param formatName The name of the {@linkplain IIOMetadata#getMetadataFormat(String) format * to use}, or {@code null} or {@code "#auto"} for an automatic selection. * @param objectClass The {@linkplain IIOMetadataFormat#getObjectClass(String) class of user * object} to locate. * * @throws IllegalArgumentException If no element accepting the given type was found, * or if more than one element accepting that type was found. * @throws NoSuchElementException If this accessor is {@linkplain #isReadOnly() is read only} * and the given metadata doesn't contains a node for the element to fetch. * * @see #listPaths(IIOMetadataFormat, Class) * * @since 3.06 */ public MetadataNodeAccessor(IIOMetadata metadata, String formatName, Class<?> objectClass) throws IllegalArgumentException, NoSuchElementException { super(metadata, formatName, objectClass); } /** * Returns {@code true} if this accessor is read-only. The default implementation returns the * read-only state of the wrapped {@linkplain #metadata} object. Subclasses can override this * method if they need more control about whatever this accessor is allowed to add child * elements in the metadata object. * <p> * If this method returns {@code true}, then every <code>setFoo(…)</code> methods in * this class will thrown an {@link UnsupportedOperationException} when they are invoked. * * @return {@code true} if this accessor is read-only, or {@code false} if it allows * write operations. * * @see IIOMetadata#isReadOnly() * * @since 3.19 */ @Override public boolean isReadOnly() { return metadata.isReadOnly(); } /** * 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(int)} method must be invoked explicitly. * * @return The index of the new child element. * @throws UnsupportedOperationException If this accessor does not allow children. * * @see #childCount() * @see #selectChild(int) */ public int appendChild() throws UnsupportedOperationException { if (isReadOnly()) { throw new UnsupportedOperationException(getErrorResources() .getString(Errors.Keys.UnmodifiableMetadata)); } final int size = childs.size(); final Node child = appendChild(parent, childPath); if (child instanceof Element) { childs.add((Element) child); return size; } else { throw new UnsupportedImplementationException(child.getClass()); } } /** * Remove a child * @param childIndex * @return * @throws UnsupportedOperationException * @throws IndexOutOfBoundsException */ public Node removeChild(int childIndex) throws UnsupportedOperationException, IndexOutOfBoundsException { if (isReadOnly()) { throw new UnsupportedOperationException(getErrorResources() .getString(Errors.Keys.UnmodifiableMetadata)); } Node child = childs.get(childIndex); if (child instanceof Element) { return removeChild(parent, childPath, child); } else { throw new UnsupportedImplementationException(child.getClass()); } } /** * Remove all child node of current element parent. * @throws UnsupportedOperationException */ public void removeChildren() throws UnsupportedOperationException { if (isReadOnly()) { throw new UnsupportedOperationException(getErrorResources() .getString(Errors.Keys.UnmodifiableMetadata)); } if (childCount() > 0) { removeChildren(parent, childPath); childs.clear(); } } /** * Returns {@code true} if values of the specified type can be formatted as a * text. We allows formatting only for reasonably cheap objects, for example * a Number but not a CoordinateReferenceSystem. */ private static boolean isFormattable(final Class<?> type) { return (type != null) && (CharSequence.class.isAssignableFrom(type) || Number.class.isAssignableFrom(Numbers.primitiveToWrapper(type))); } /** * Formats a sequence for {@link #setAttribute} implementations working on list arguments. * * @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(48); final int length = Array.getLength(values); for (int i=0; i<length; i++) { if (i != 0) { buffer.append(' '); } final Object value = Array.get(values, i); if (value != null) { final String s = value.toString().trim(); final int sl = s.length(); for (int j=0; j<sl; j++) { final char c = s.charAt(j); buffer.append(Character.isWhitespace(c) ? NBSP : c); } } } text = buffer.length() != 0 ? buffer.toString() : null; } return text; } /** * Sets the {@linkplain IIOMetadataNode#setUserObject user object} associated with the * {@linkplain #selectChild selected element}. At the difference of every {@code setAttribute} * methods defined in this class, this method does not delegate to * {@link #setAttribute(String, String)}. * <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 * user objects, since they are particular to Image I/O metadata. * * @param value The user object, or {@code null} if none. * @throws UnsupportedImplementationException if the selected element is not an instance of * {@link IIOMetadataNode}. * * @see #getUserObject() */ public void setUserObject(final Object value) throws UnsupportedImplementationException { final Element element = currentElement(); String asText = null; if (value != null) { final Class<?> type = value.getClass(); if (Date.class.isAssignableFrom(type)) { asText = JDK8.printDateTime((Date) value); } else if (isFormattable(type)) { asText = value.toString(); } else if (isFormattable(type.getComponentType())) { asText = formatSequence(value); } } if (element instanceof IIOMetadataNode) { ((IIOMetadataNode) element).setUserObject(value); } else if (value!=null && asText==null) { throw new UnsupportedImplementationException(getErrorResources().getString( Errors.Keys.IllegalClass_2, Classes.getClass(element), IIOMetadataNode.class)); } element.setNodeValue(asText); } /** * Sets the attribute to the specified value, or remove the attribute if the value is null. * <p> * Every {@code setAttribute} methods in this class invoke this method last. Consequently, * this method provides a single overriding point for subclasses that want to process the * attribute after formatting. * * @param attribute The attribute name. * @param value The attribute value, or {@code null} for removing the attribute. * * @see #getAttribute(String) * @see IIOMetadataFormat#DATATYPE_STRING */ public void setAttribute(final String attribute, String value) { ensureNonNull("attribute", attribute); if (isReadOnly()) { throw new UnsupportedOperationException(getErrorResources() .getString(Errors.Keys.UnmodifiableMetadata)); } final Element element = currentElement(); if (value == null || (value=value.trim()).isEmpty()) { if (element.hasAttribute(attribute)) { element.removeAttribute(attribute); } } else { element.setAttribute(attribute, value); } } /** * Sets the attribute to the specified array of values, or remove the attribute if the array * is {@code null}. The given items are formatted in a single string with an ordinary space * used as the item separator, as mandated by {@link IIOMetadataFormat#VALUE_LIST}. If some * of the given items contain spaces, then those spaces are replaced by a no-break space * (<code>'\\u00A0'</code>) for avoiding confusion with the space separator. * * @param attribute The attribute name. * @param values The attribute values, or {@code null} for removing the attribute. * * @see #getAttributeAsStrings(String, boolean) * * @since 3.06 */ public void setAttribute(final String attribute, final String... values) { setAttribute(attribute, formatSequence(values)); } /** * Sets the attribute to the specified code value, or remove the attribute if the value is null. * * @param attribute The attribute name. * @param value The attribute value, or {@code null} for removing the attribute. * * @see #getAttributeAsCode(String, Class) * * @since 3.06 */ public void setAttribute(final String attribute, final ControlledVocabulary value) { setAttribute(attribute, Types.getCodeName(value)); } /** * Sets the attribute to the specified boolean value. * * @param attribute The attribute name. * @param value The attribute value. * * @see #getAttributeAsBoolean(String) * @see IIOMetadataFormat#DATATYPE_BOOLEAN * * @since 3.06 */ public void setAttribute(final String attribute, final boolean value) { setAttribute(attribute, Boolean.toString(value)); } /** * Sets the attribute to the specified integer value. * * @param attribute The attribute name. * @param value The attribute value. * * @see #getAttributeAsInteger(String) * @see IIOMetadataFormat#DATATYPE_INTEGER */ public void setAttribute(final String attribute, final int value) { setAttribute(attribute, Integer.toString(value)); } /** * 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 values The attribute values, or {@code null} for removing the attribute. * * @see #getAttributeAsIntegers(String, boolean) */ public void setAttribute(final String attribute, final int... values) { setAttribute(attribute, formatSequence(values)); } /** * Sets the attribute to the specified floating point value, * or remove the attribute if the value is NaN or infinity. * * @param attribute The attribute name. * @param value The attribute value. * * @see #getAttributeAsFloat(String) * @see IIOMetadataFormat#DATATYPE_FLOAT * * @since 3.06 */ public void setAttribute(final String attribute, final float value) { String text = null; if (!Float.isNaN(value) && !Float.isInfinite(value)) { text = Float.toString(value); } setAttribute(attribute, text); } /** * 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 values The attribute values, or {@code null} for removing the attribute. * * @see #getAttributeAsDoubles(String, boolean) * * @since 3.06 */ public void setAttribute(final String attribute, final float... values) { setAttribute(attribute, formatSequence(values)); } /** * Sets the attribute to the specified floating point value, * or remove the attribute if the value is NaN or infinity. * * @param attribute The attribute name. * @param value The attribute values. * * @see #getAttributeAsDouble(String) * @see IIOMetadataFormat#DATATYPE_DOUBLE */ public void setAttribute(final String attribute, final double value) { String text = null; if (!Double.isNaN(value) && !Double.isInfinite(value)) { text = Double.toString(value); } setAttribute(attribute, text); } /** * 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 values The attribute values, or {@code null} for removing the attribute. * * @see #getAttributeAsDoubles(String, boolean) */ public void setAttribute(final String attribute, final double... values) { setAttribute(attribute, formatSequence(values)); } /** * Sets the attribute to the specified value, or remove the attribute if the value is null. * * @param attribute The attribute name. * @param value The attribute value, or {@code null} for removing the attribute. * * @see #getAttributeAsDate(String) */ public void setAttribute(final String attribute, final Date value) { String text = null; if (value != null) { if (metadata instanceof SpatialMetadata) { text = ((SpatialMetadata) metadata).dateFormat().format(value); } else { // Inefficient fallback, but should usually not happen anyway. text = SpatialMetadata.format(Date.class, value); } } setAttribute(attribute, text); } /** * Sets the attribute to the specified range value. * * @param attribute The attribute name. * @param value The attribute value, or {@code null} for removing the attribute. * * @see #getAttributeAsRange(String) * * @since 3.06 */ public void setAttribute(final String attribute, final NumberRange<?> value) { String text = null; if (value != null) { if (metadata instanceof SpatialMetadata) { text = ((SpatialMetadata) metadata).rangeFormat().format(value); } else { // Inefficient fallback, but should usually not happen anyway. text = SpatialMetadata.format(NumberRange.class, value); } } setAttribute(attribute, text); } /** * Sets the attribute to the specified unit value. * * @param attribute The attribute name. * @param value The attribute value. * * @see #getAttributeAsUnit(String, Class) * * @since 3.07 */ public void setAttribute(final String attribute, final Unit<?> value) { setAttribute(attribute, (value != null) ? value.toString() : null); } /** * Sets the attribute to the specified citation value, * or remove the attribute if the value is null. * * @param attribute The attribute name. * @param value The attribute value, or {@code null} for removing the attribute. * * @see #getAttributeAsCitation(String) * * @since 3.06 */ public void setAttribute(final String attribute, final Citation value) { setAttribute(attribute, Citations.getIdentifier(value)); } }