/*
* 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.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import javax.imageio.ImageReader; // For javadoc
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.metadata.IIOMetadataFormat;
import javax.measure.Quantity;
import javax.measure.Unit;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.opengis.util.CodeList;
import org.opengis.metadata.citation.Citation;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.gui.swing.tree.Trees;
import org.geotoolkit.image.io.WarningProducer;
import org.apache.sis.util.iso.Types;
import org.geotoolkit.internal.image.io.Warnings;
import org.apache.sis.measure.Units;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.Localized;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.Classes;
import org.apache.sis.util.Numbers;
import org.geotoolkit.metadata.Citations;
import org.apache.sis.util.resources.IndexedResourceBundle;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* Convenience class for reading attribute values from 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>
*
* After a {@code MetadataNodeParser} 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}.
*
* {@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 MetadataNodeParser} 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 MetadataNodeParser(..., "RectifiedGridDomain/CRS/CoordinateSystem", "Axis")}</li>
* <li>{@code new MetadataNodeParser(..., "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 MetadataNodeParser} 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);
* MetadataNodeParser accessor = new MetadataNodeParser(metadata, null,
* "RectifiedGridDomain/CRS/CoordinateSystem", "Axis");
*
* accessor.selectParent();
* String csName = accessor.getAttribute("name");
*
* accessor.selectChild(0);
* String firstAxisName = accessor.getAttribute("name");
* }
*
* {@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.
* 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 MetadataNodeParser implements WarningProducer {
/**
* The separator between names in a node path.
*/
private static final char SEPARATOR = '/';
/**
* No-break space. This is used for replacing ordinary spaces in a string which is to
* be included in a list of strings. We can not keep the ordinary space since it is the
* item separator.
*/
static final char NBSP = '\u00A0';
/**
* The Image I/O metadata for which this accessor is a wrapper. An instance
* of the {@link SpatialMetadata} subclass is recommended, but not mandatory.
*
* @since 3.06
*/
protected final IIOMetadata metadata;
/**
* The metadata format used by this accessor.
*/
final IIOMetadataFormat format;
/**
* The parent of child {@linkplain Element elements}.
*/
final Node parent;
/**
* The {@linkplain #childs} path. This is the {@code childPath} parameter
* given to the constructor if explicitly specified, or the computed value
* if the parameter given to the constructor was {@code "#auto"}.
*/
final String childPath;
/**
* The list of child elements. May be empty but never null. This list is non-modifiable if
* {@link #childPath} is {@code null}. Otherwise, new elements can be added to this list.
*/
final List<Node> childs;
/**
* The current element, or {@code null} if not yet selected.
*
* @see #selectChild
* @see #currentElement()
*/
private transient Element current;
/**
* The logging level for the warnings, or {@link Level#OFF} if disabled.
*/
private transient Level warningLevel;
/**
* 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 MetadataNodeParser(final MetadataNodeParser clone) {
metadata = clone.metadata;
format = clone.format;
parent = clone.parent;
childPath = clone.childPath;
childs = clone.childs;
current = clone.current;
warningLevel = clone.warningLevel;
}
/**
* 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
* MetadataNodeParser parent = new MetadataNodeParser(..., "DiscoveryMetadata/Extent", ...);
* MetadataNodeParser child = new MetadataNodeParser(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 the given metadata doesn't contains a node
* for the element to fetch.
*/
public MetadataNodeParser(MetadataNodeParser parent, String path, String childPath)
throws IllegalArgumentException, NoSuchElementException
{
this(parent, parent.metadata, null, null, 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 the given metadata doesn't contains a node for
* the element to fetch.
*
* @see #listPaths(IIOMetadataFormat, Class)
*/
public MetadataNodeParser(MetadataNodeParser parent, Class<?> objectClass)
throws IllegalArgumentException, NoSuchElementException
{
this(parent, parent.metadata, null, objectClass, null, "#auto");
}
/**
* 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 #MetadataNodeParser(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 the given metadata doesn't contains a node for the
* element to fetch.
*/
public MetadataNodeParser(IIOMetadata metadata, String parentPath)
throws NoSuchElementException
{
this(metadata, "#auto", parentPath, "#auto");
}
/**
* 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 #MetadataNodeParser(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 the given metadata doesn't contains a node for the
* element to fetch.
*/
public MetadataNodeParser(IIOMetadata metadata, String formatName, String parentPath,
String childPath) throws NoSuchElementException
{
this(null, metadata, formatName, null, 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 MetadataNodeParser(metadata, formatName, GeographicElement.class);
* }
*
* is equivalent to:
*
* {@preformat java
* new MetadataNodeParser(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 the given metadata doesn't contains a node for
* the element to fetch.
*
* @see #listPaths(IIOMetadataFormat, Class)
*
* @since 3.06
*/
public MetadataNodeParser(IIOMetadata metadata, String formatName, Class<?> objectClass)
throws IllegalArgumentException, NoSuchElementException
{
this(null, metadata, formatName, objectClass, null, "#auto");
}
/**
* Implementation of the public constructors. If the {@code parentAccessor}
* argument is non-null, then {@code parentPath} is relative to that parent.
* <p>
* The {@code type} argument and the ({@code parentPath}, {@code childPath})
* pair of arguments are exclusive (only one of them shall be non-null).
*
* @param parent The accessor for which the {@code parentPath} is relative, or from
* which to start the search for an element accepting the given type.
* @param metadata The Image I/O metadata to wrap.
* @param formatName The name of the format to use, or {@code null} or {@code "#auto"}.
* @param type The class of user object to locate, or {@code null} for explicit paths.
* @param parentPath The path to the node of interest, or {@code null} for the root.
* @param childPath The relative path to the child elements, or {@code null} if none,
* or {@code "#auto"} for auto-detection.
*/
@SuppressWarnings("fallthrough")
private MetadataNodeParser(final MetadataNodeParser parentAccessor, IIOMetadata metadata,
String formatName, final Class<?> type, String parentPath, String childPath)
throws IllegalArgumentException, NoSuchElementException
{
ensureNonNull("metadata", metadata);
IIOMetadataFormat format;
Node root;
/*
* The following loop is typically executed exactly once. It will be executed more than
* once only if the requested type (if non-null) was not found in the given metadata,
* and a fallback exists. In such case, the fallbacks will be iteratively examined.
*
* To be honest, we are actually using this loop construct as a "goto" statement. The
* pseudo-goto is the "continue" statement near the end of this loop, just before the
* "throw new IllegalArgumentException" statement.
*
* This block will assign or modify the following variables:
*
* - metadata (if it was necessary to iterate in the fallback chain)
* - format (derived from metadata)
* - root (derived from metadata)
* - parentPath (if a non-null type was specified)
*/
while (true) {
/*
* Fetch the IIOMetadataFormat to use and the root of the tree,
* or the root of the sub-tree if 'parentAccessor' is non-null.
*/
SpatialMetadata sp = null; // can be non-null only for "#auto" (or null) format.
if (parentAccessor != null) {
format = parentAccessor.format;
root = parentAccessor.parent;
warningLevel = parentAccessor.warningLevel;
} else if (formatName != null && !formatName.equals("#auto")) {
format = metadata.getMetadataFormat(formatName);
root = metadata.getAsTree(formatName);
} else if (metadata instanceof SpatialMetadata) {
sp = (SpatialMetadata) metadata;
format = sp.format;
root = sp.getAsTree();
} else {
// In preference order: native, standard, extra formats.
formatName = metadata.getMetadataFormatNames()[0];
format = metadata.getMetadataFormat(formatName);
root = metadata.getAsTree(formatName);
}
if (format == null) {
throw new IllegalArgumentException(getErrorResources().getString(
Errors.Keys.UndefinedFormat_1, formatName));
}
// If the user did not provided a Class<?> argument, we are done.
if (type == null) {
break;
}
/*
* If the caller asked for a node associated to a user object of the
* given type, get the path to that node. We expect a single path.
*/
final List<String> paths = new ArrayList<>(4);
listPaths(format, type, root.getNodeName(), new StringBuilder(48), paths);
final int count = paths.size();
if (count == 1) {
// Found the path we were looking for. Stop the search.
parentPath = paths.get(0);
break;
}
if (count != 0) {
// Found too many paths.
final String lineSeparator = System.lineSeparator();
final StringBuilder buffer = new StringBuilder(getErrorResources().getString(
Errors.Keys.AmbiguousValue_1, type)).append(lineSeparator);
for (final String path : paths) {
buffer.append(" \u2022 ").append(path).append(lineSeparator);
}
throw new IllegalArgumentException(buffer.toString());
}
/*
* Found no path. If there is a fallback, get the fallback and redo all the
* process from the begining of this method. Otherwise throw an exception.
* Note that 'sp' is non-null only if the format name is "#auto" (or null).
*/
if (sp != null) {
metadata = sp.fallback;
if (metadata != null) {
continue;
}
}
throw new IllegalArgumentException(getErrorResources()
.getString(Errors.Keys.UnknownType_1, type));
}
/*
* End of the pseudo-goto block construct.
* At this point we have the final metadata, format and root node.
*/
this.metadata = metadata;
this.format = format;
if (warningLevel == null) {
warningLevel = (metadata instanceof SpatialMetadata) ?
((SpatialMetadata) metadata).getWarningLevel() : Level.WARNING;
}
/*
* Fetch 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<>(4);
if (parentPath != null) {
listChilds(root, parentPath, 0, childs, true);
final int count = childs.size();
switch (count) {
default: {
warning("<init>", Errors.Keys.TooManyOccurrences_2, parentPath, count);
// Fall through for picking the first node.
}
case 1: {
parent = childs.get(0);
childs.clear();
break;
}
case 0: {
if (isReadOnly()) {
throw new NoSuchElementException(getErrorResources().getString(
Errors.Keys.NoSuchElementName_1, parentPath));
}
parent = appendChild(root, parentPath);
break;
}
}
} else {
parent = root;
}
/*
* If the child is "#auto", get the name from the metadata format. We will pick the
* child name only if there is no ambiguity: only one child with the repeat policy.
* If we can not find an unambiguous child, we will process as if there is no child.
*/
if ("#auto".equals(childPath)) {
childPath = null;
final String name = parent.getNodeName();
if (format.getChildPolicy(name) == IIOMetadataFormat.CHILD_POLICY_REPEAT) {
final String[] childNames = format.getChildNames(name);
if (childNames != null && childNames.length == 1) {
childPath = childNames[0];
}
}
}
/*
* 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 unexpected metadata format 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.
*/
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 IIONode(name.intern()));
}
final String name = path.substring(lower).trim().intern();
return parent.appendChild(new IIONode(name));
}
/**
* Remove a child from a parent
* @param parent The parent to add a child to.
* @param path The path of the child to add.
* @param child The child to remove
* @return element The removed child
*/
Node removeChild(Node parent, final String path, Node child) {
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 IIONode(name.intern()));
}
Node removed = parent.removeChild(child);
if (current.equals(removed)) {
if (parent instanceof Element) {
current = (Element) parent;
}
}
return removed;
}
/**
* Remove all child node from parent.
* @param parent The parent to add a child to.
* @param path The path of the child to add.
*/
void removeChildren(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 IIONode(name.intern()));
}
while (parent.hasChildNodes()) {
parent.removeChild(parent.getLastChild());
}
if (parent instanceof Element) {
current = (Element) parent;
}
}
/**
* Returns the paths to every {@linkplain Element elements} declared in the given format which
* accept a {@linkplain IIOMetadataNode#getUserObject() user object} of the given type. If no
* path is found, returns an empty list.
*
* {@section Collection handling}
* In the particular case where the element is the child of a node having having
* {@link IIOMetadataFormat#CHILD_POLICY_REPEAT CHILD_POLICY_REPEAT}, then the path to the
* parent node is returned. In other words, if the given type is the type of elements in a
* collection, then the path to the whole collection is returned instead than the path to a
* single element in that collection.
*
* @param format The metadata format in which to search.
* @param objectClass The {@linkplain IIOMetadataFormat#getObjectClass(String)
* class of user object} to locate.
* @return The list of paths to elements that accept user objects of the given type.
*
* @see #MetadataNodeParser(MetadataNodeParser, Class)
* @see #MetadataNodeParser(IIOMetadata, String, Class)
*
* @since 3.06
*/
public static List<String> listPaths(final IIOMetadataFormat format, final Class<?> objectClass) {
ensureNonNull("type", objectClass);
ensureNonNull("format", format);
final List<String> paths = new ArrayList<>(4);
listPaths(format, objectClass, format.getRootName(), new StringBuilder(48), paths);
return paths;
}
/**
* Adds to the given list the path to every elements of the given type.
* This method invokes itself recursively for scanning down the tree.
*
* @param format The metadata format in which to search.
* @param objectClass The type of user object to locate.
* @param elementName The current element to be scaned.
* @param buffer An initially empty buffer, for internal use by this method.
* @param paths The list where to add the paths that we found.
*/
private static void listPaths(final IIOMetadataFormat format, final Class<?> objectClass,
final String elementName, final StringBuilder buffer, final List<String> paths)
{
final int childPolicy = format.getChildPolicy(elementName);
if (childPolicy != IIOMetadataFormat.CHILD_POLICY_EMPTY) {
final String[] childs = format.getChildNames(elementName);
if (childs != null) {
final int base = buffer.length();
for (final String child : childs) {
if (base != 0) {
buffer.append('/');
}
buffer.append(child);
if (format.getObjectValueType(child) != IIOMetadataFormat.VALUE_NONE) {
final Class<?> candidate = format.getObjectClass(child);
if (objectClass != null && objectClass.isAssignableFrom(candidate)) {
/*
* We found an element. If this element is to be repeated in a collection,
* then use the path of the parent which describe the whole collection.
*/
paths.add(childPolicy == IIOMetadataFormat.CHILD_POLICY_REPEAT ?
buffer.substring(0, base) : buffer.toString());
}
}
listPaths(format, objectClass, child, buffer, paths);
buffer.setLength(base);
}
}
}
}
/**
* Returns the name of the element for which this accessor will fetch attribute values.
* This is the last part of the {@code parentPath} argument given at construction time.
* For example if the given path was {@code "DiscoveryMetadata/Extent/GeographicElement"},
* then the name returned by this method is {@code "GeographicElement"}.
*
* @return The name of the metadata element.
*
* @since 3.06
*/
public String name() {
return parent.getNodeName();
}
/**
* Returns {@code true} if this accessor is read-only. The default implementation returns
* {@code true} in every case, since this {@code MetadataNodeParser} is only for reading
* attributes. The {@link MetadataNodeAccessor} sub-classe will override this method with
* a different behavior, and make it public.
* <p>
* Note that this method is invoked by the constructors.
*
* @return {@code true} if this accessor is read-only, or {@code false} if it allows
* write operations.
*/
boolean isReadOnly() {
return true;
}
/**
* Returns {@code true} if this node contains no child and no attribute.
*
* @return {@code true} if this node is empty, or {@code false} if it contains at
* least one child or one attribute.
*
* @since 3.13
*/
public boolean isEmpty() {
return childs.isEmpty() && !currentElement().hasAttributes();
}
/**
* Returns the number of child {@linkplain Element elements}.
* This is the upper value (exclusive) for {@link #selectChild(int)}.
*
* @return The child {@linkplain Element elements} count.
*
* @see #selectChild(int)
* @see MetadataNodeAccessor#appendChild()
*/
public int childCount() {
return childs.size();
}
/**
* Returns {@code true} if this accessor allows children. If this method returns
* {@code false}, then attempts to {@linkplain MetadataNodeAccessor#appendChild()
* append a child} will throw a {@link UnsupportedOperationException}.
*
* @return {@code true) if this accessor allows children.
*/
final boolean allowsChildren() {
return childs != Collections.EMPTY_LIST;
}
/**
* 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 MetadataNodeAccessor#appendChild()
* @see #selectParent()
*/
public 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(int)
*/
public 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(int)
*/
final Element currentElement() throws IllegalStateException {
if (current == null) {
throw new IllegalStateException();
}
return current;
}
/**
* Returns the {@linkplain IIOMetadataNode#getUserObject() user object} associated with the
* {@linkplain #selectChild(int) selected element}, or {@code null} if none. This method returns
* the first of the following methods which return a non-null value:
* <p>
* <ul>
* <li>{@link IIOMetadataNode#getUserObject()} (only if the node is an instance of {@code IIOMetadata})</li>
* <li>{@link Node#getNodeValue()}</li>
* </ul>
* <p>
* The <cite>node value</cite> fallback is consistent with {@link MetadataNodeAccessor#setUserObject(Object)}
* implementation, and allows processing of nodes that are not {@link IIOMetadataNode} instances.
*
* {@note This <code>getUserObject()</code> method and the <code>getUserObject(Class)</code>
* method below are the only getters that do not fetch the string to parse by a call
* to <code>getAttribute</code>.}
*
* @return The user object, or {@code null} if none.
*
* @see #getUserObject(Class)
* @see MetadataNodeAccessor#setUserObject(Object)
*/
public 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 as an instance of the specified class. This method first invokes
* {@link #getUserObject()}, then checks the type of the returned object. The type shall be
* the requested one - this method does not attempt conversions.
* <p>
* A special processing is performed if the type of the user object is assignable to
* {@link CharSequence}. This special processing is performed because if the node is
* not an instance of {@link IIOMetadataNode}, then {@code getUserObject()} fallbacks
* on {@link Node#getNodeValue()}, which can return only a {@code String}. In such
* case, this method will attempt to parse the string if the requested type is
* a subclass of {@link Number}, {@link Date}, {@code double[]} or {@code int[]}.
*
* @param <T> The expected class.
* @param type The expected class.
* @return The user object, or {@code null} if none.
* @throws NumberFormatException If attempt to parse the user object as a number failed.
* @throws IllegalArgumentException If attempt to parse the user object as a date failed.
* @throws ClassCastException If the user object can not be casted to the specified type.
*
* @see #getUserObject()
* @see MetadataNodeAccessor#setUserObject(Object)
* @see SpatialMetadata#getInstanceForType(Class)
*/
public <T> T getUserObject(final Class<? extends T> type) throws ClassCastException {
ensureNonNull("type", type);
Object value = getUserObject();
if (value instanceof CharSequence) {
if (String.class.isAssignableFrom(type)) {
value = value.toString();
} else if (Number.class.isAssignableFrom(type)) {
value = Numbers.valueOf(value.toString(), type);
} else if (Date.class.isAssignableFrom(type)) {
value = org.geotoolkit.internal.jdk8.JDK8.parseDateTime(value.toString());
} else if (type.isArray()) {
final Class<?> component = Numbers.primitiveToWrapper(type.getComponentType());
if (component == Double.class) {
value = parseSequence(value.toString(), Double.TYPE, false, null);
} else if (component == Integer.class) {
value = parseSequence(value.toString(), Integer.TYPE, false, null);
}
}
}
return type.cast(value);
}
/**
* Returns a view of the {@linkplain #selectParent() parent element} as an implementation of
* the given interface. This method returns an instance of the given interface where each
* getter method is implemented like the following pseudo-code, where {@code <T>} is the type
* given in argument to this method, {@code <RT1>} is the return type of the first method
* and {@code <RT2>} is the return type of the second method:
*
* {@preformat java
* class Proxy implements <T> {
* public <RT1> getBanana() {
* return Accessor.this.getAttributeAs<RT1>("banana");
* }
*
* public <RT2> getApple() {
* return Accessor.this.getAttributeAs<RT2>("apple");
* }
*
* // etc. for every getter methods declared in the interface.
* }
* }
*
* The {@code <T>} type is typically one of the types given to the
* {@link SpatialMetadataFormat#addTree(org.geotoolkit.metadata.MetadataStandard,
* Class, String, String, java.util.Map) SpatialMetadataFormat.addTree(...)} method,
* but this is not mandatory. This {@code <T>} is usually an interface from the ISO
* 19115-2 standard, but this is not mandatory neither. However all getter methods
* declared in that type shall comply with the <cite>Java Beans</cite> conventions.
*
* {@section Example}
* Assume a metadata format conforms to the
* <a href="SpatialMetadataFormat.html#default-formats"><cite>Stream metadata</cite>
* format documented here</a>. There is an extract of that format:
*
* {@preformat text
* DiscoveryMetadata
* └───SpatialResolution
* └───distance
* }
*
* In the following code, every call to the
* {@link org.opengis.metadata.identification.Resolution#getDistance()} method on the instance
* returned by this {@code newProxyInstance(type)} method will be implemented as a call to the
* <code>{@linkplain #getAttributeAsDouble(String) getAttributeAsDouble}("distance")</code>
* method on this {@code MetadataNodeParser} instance:
*
* {@preformat java
* IIOMetadata metadata = new SpatialMetadata(SpatialMetadataFormat.STREAM);
* MetadataNodeParser accessor = new MetadataNodeParser(metadata, "#auto", "DiscoveryMetadata/SpatialResolution", null);
* SpatialResolution resolution = accessor.newProxyInstance(SpatialResolution.class);
*
* // From this point, we can forget that the metadata are stored in an IIOMetadata object.
* // The following line delegates the work to accessor.getAttributeAsDouble("distance");
* Double distance = resolution.getDistance();
* System.out.println("The resolution is " + distance);
* }
*
* Changes to the underlying {@code IIOMetadata} attributes are immediately reflected in the
* {@code Resolution} instance:
*
* {@preformat java
* accessor.setAttribute("distance", 20);
* distance = resolution.getDistance(); // Should now return 20.
* }
*
* {@section Nested proxies}
* If the return type of a getter method (the {@code <RT1>} and {@code <RT2>} types in the
* above example) is not assignable from {@link String}, {@link Double} or other types for
* which a {@code getAttribute} method is defined in this {@code MetadataNodeParser} class
* (see <a href="package-summary.html#accessor-types">here</a> for a complete list), then
* that type is assumed to be an other metadata interface. In such case, a new
* {@code MetadataNodeParser} is created for that element and a new proxy created by
* this {@code newProxyInstance} method is returned.
*
* @param <T> The compile-time type specified as the {@code type} argument.
* @param type The interface for which to create a proxy instance.
* @return An implementation of the given interface with getter methods that fetch
* their return values from the attribute values.
* @throws IllegalArgumentException If the given type is not a valid interface.
*
* @see SpatialMetadata#getInstanceForType(Class)
* @see java.lang.reflect.Proxy
*
* @since 3.06
*/
public <T> T newProxyInstance(final Class<T> type) throws IllegalArgumentException {
return MetadataProxy.newProxyInstance(type, this);
}
/**
* Returns a view of the {@linkplain #selectChild(int) child elements} as a list
* of implementations of the given type. This method performs the same work than
* {@link #newProxyInstance(Class)} for every childs of the element represented
* by this accessor.
*
* @param <T> The compile-time type specified as the {@code type} argument.
* @param type The interface for which to create proxy instances.
* @return A list of implementations of the given interface with getter methods
* that fetch their return values from the attribute values.
* @throws IllegalArgumentException If the given type is not a valid interface.
*
* @see SpatialMetadata#getListForType(Class)
*
* @since 3.06
*/
public <T> List<T> newProxyList(final Class<T> type) throws IllegalArgumentException {
if (allowsChildren()) {
return MetadataProxyList.create(type, this);
} else {
return Collections.singletonList(MetadataProxy.newProxyInstance(type, this));
}
}
/**
* 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 getAttribute} methods in this class invoke this method first. Consequently,
* this method provides a single overriding point for subclasses that 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.
*
* @see MetadataNodeAccessor#setAttribute(String, String)
* @see IIOMetadataFormat#DATATYPE_STRING
*/
public String getAttribute(final String attribute) {
ensureNonNull("attribute", attribute);
String candidate = currentElement().getAttribute(attribute);
if (candidate != null) {
candidate = candidate.trim();
if (candidate.isEmpty()) {
candidate = null;
}
}
return candidate;
}
/**
* Returns an attribute as an array of strings for the {@linkplain #selectChild selected element},
* or {@code null} if none. This method gets the attribute as a single string, then splits that
* string on the ordinary space separator. If some items in the resulting strings contain the
* no-break space (<code>'\\u00A0'</code>), then those characters are replaced by ordinary spaces
* after the split.
*
* @param attribute The attribute to fetch (e.g. {@code "keywords"}).
* @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.
*
* @see MetadataNodeAccessor#setAttribute(String, String[])
*
* @since 3.06
*/
public String[] getAttributeAsStrings(final String attribute, final boolean unique) {
return (String[]) parseSequence(getAttribute(attribute), String.class, unique, "getAttributeAsStrings");
}
/**
* Returns an attribute as a code for the {@linkplain #selectChild selected element},
* or {@code null} if none. If the code stored in the given attribute is not a known
* element, then this method logs a warning and returns {@code null}.
*
* @param <T> The type of the code list.
* @param attribute The attribute to fetch (e.g. {@code "imagingCondition"}).
* @param codeType The type of the code list. This is used for determining the expected values.
* @return The attribute value, or {@code null} if none or unknown.
*
* @see MetadataNodeAccessor#setAttribute(String, CodeList)
*
* @since 3.06
*/
public <T extends CodeList<T>> T getAttributeAsCode(final String attribute, final Class<T> codeType) {
final String value = getAttribute(attribute);
final T code = Types.forCodeName(codeType, value, false);
if (code == null && value != null) {
warning("getAttributeAsCode", Errors.Keys.IllegalParameterValue_2, attribute, value);
}
return code;
}
/**
* Returns an attribute as an enumeration value for the {@linkplain #selectChild selected element},
* or {@code null} if none. If the enumeration value stored in the given attribute is not a known
* element, then this method logs a warning and returns {@code null}.
*
* @param <T> The type of the enumeration.
* @param attribute The attribute to fetch.
* @param codeType The type of the enumeration. This is used for determining the expected values.
* @return The attribute value, or {@code null} if none or unknown.
*
* @since 4.0
*/
public <T extends Enum<T>> T getAttributeAsEnum(final String attribute, final Class<T> codeType) {
final String value = getAttribute(attribute);
final T code = Types.forEnumName(codeType, value);
if (code == null && value != null) {
warning("getAttributeAsEnum", Errors.Keys.IllegalParameterValue_2, attribute, value);
}
return code;
}
/**
* Returns an attribute as a boolean for the {@linkplain #selectChild selected element},
* or {@code null} if none. If the attribute can't be parsed as a boolean, then this
* method logs a warning and returns {@code null}.
*
* @param attribute The attribute to fetch (e.g. {@code "inclusion"}).
* @return The attribute value, or {@code null} if none or unparseable.
*
* @see MetadataNodeAccessor#setAttribute(String, boolean)
* @see IIOMetadataFormat#DATATYPE_BOOLEAN
*
* @since 3.06
*/
public Boolean getAttributeAsBoolean(final String attribute) {
final String value = getAttribute(attribute);
if (value != null) {
if (value.equalsIgnoreCase("true") ||
value.equalsIgnoreCase("yes") ||
value.equalsIgnoreCase("on"))
{
return Boolean.TRUE;
}
if (value.equalsIgnoreCase("false") ||
value.equalsIgnoreCase("no") ||
value.equalsIgnoreCase("off"))
{
return Boolean.FALSE;
}
warning("getAttributeAsBoolean", Errors.Keys.IllegalParameterValue_2, attribute, value);
}
return null;
}
/**
* 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.
*
* @see MetadataNodeAccessor#setAttribute(String, int)
* @see IIOMetadataFormat#DATATYPE_INTEGER
*/
public Integer getAttributeAsInteger(final String attribute) {
String value = getAttribute(attribute);
if (value != null) {
value = CharSequences.trimFractionalPart(value).toString();
try {
return Integer.valueOf(value);
} catch (NumberFormatException e) {
warning("getAttributeAsInteger", Errors.Keys.UnparsableNumber_1, value);
}
}
return null;
}
/**
* 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.
*
* @see MetadataNodeAccessor#setAttribute(String, int[])
*/
public int[] getAttributeAsIntegers(final String attribute, final boolean unique) {
return (int[]) parseSequence(getAttribute(attribute), Integer.TYPE, unique, "getAttributeAsIntegers");
}
/**
* Returns an attribute as a floating point values 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.
*
* @see MetadataNodeAccessor#setAttribute(String, float)
* @see IIOMetadataFormat#DATATYPE_FLOAT
*
* @since 3.06
*/
public Float getAttributeAsFloat(final String attribute) {
final String value = getAttribute(attribute);
if (value != null) try {
return Float.valueOf(value);
} catch (NumberFormatException e) {
warning("getAttributeAsFloat", Errors.Keys.UnparsableNumber_1, value);
}
return null;
}
/**
* Returns an attribute as an array of floating point values 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.
*
* @see MetadataNodeAccessor#setAttribute(String, float[])
*
* @since 3.06
*/
public float[] getAttributeAsFloats(final String attribute, final boolean unique) {
return (float[]) parseSequence(getAttribute(attribute), Float.TYPE, unique, "getAttributeAsFloats");
}
/**
* 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.
*
* @see MetadataNodeAccessor#setAttribute(String, double)
* @see IIOMetadataFormat#DATATYPE_DOUBLE
*/
public Double getAttributeAsDouble(final String attribute) {
final String value = getAttribute(attribute);
if (value != null) try {
return Double.valueOf(value);
} catch (NumberFormatException e) {
warning("getAttributeAsDouble", Errors.Keys.UnparsableNumber_1, value);
}
return null;
}
/**
* 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.
*
* @see MetadataNodeAccessor#setAttribute(String, double[])
*/
public double[] getAttributeAsDoubles(final String attribute, final boolean unique) {
return (double[]) parseSequence(getAttribute(attribute), Double.TYPE, unique, "getAttributeAsDoubles");
}
/**
* Returns an attribute as a date for the {@linkplain #selectChild selected element},
* or {@code null} if none. If the attribute can't be parsed as a date, then this method
* logs a warning and returns {@code null}.
*
* @param attribute The attribute to fetch (e.g. {@code "origin"}).
* @return The attribute value, or {@code null} if none or unparseable.
*
* @see MetadataNodeAccessor#setAttribute(String, Date)
*/
public Date getAttributeAsDate(final String attribute) {
String value = getAttribute(attribute);
if (value != null) {
value = CharSequences.trimFractionalPart(value).toString();
if (metadata instanceof SpatialMetadata) {
return ((SpatialMetadata) metadata).dateFormat().parse(value);
} else try {
// Inefficient fallback, but should usually not happen anyway.
return SpatialMetadata.parse(Date.class, value);
} catch (ParseException e) {
warning(null, MetadataNodeParser.class, "getAttributeAsDate", e);
}
}
return null;
}
/**
* Returns an attribute as a range of numbers for the {@linkplain #selectChild selected element},
* or {@code null} if none. If the attribute can't be parsed as a range of numbers, then this
* method logs a warning and returns {@code null}.
*
* @param attribute The attribute to fetch (e.g. {@code "validSampleValues"}).
* @return The attribute value, or {@code null} if none or unparseable.
*
* @see MetadataNodeAccessor#setAttribute(String, NumberRange)
*
* @since 3.06
*/
public NumberRange<?> getAttributeAsRange(final String attribute) {
final String value = getAttribute(attribute);
if (value != null) {
if (metadata instanceof SpatialMetadata) {
return ((SpatialMetadata) metadata).rangeFormat().parse(value);
} else try {
// Inefficient fallback, but should usually not happen anyway.
return (NumberRange<?>) SpatialMetadata.parse(NumberRange.class, value);
} catch (ParseException e) {
warning(null, MetadataNodeParser.class, "getAttributeAsRange", e);
}
}
return null;
}
/**
* Returns an attribute as a unit for the {@linkplain #selectChild selected element},
* or {@code null} if none. If the attribute can't be parsed as a unit of the given
* quantity, then this method logs a warning and returns {@code null}.
*
* @param <Q> The compile-time type of the {@code quantity} argument.
* @param attribute The attribute to fetch (e.g. {@code "axisUnit"}).
* @param quantity The quantity of the unit to be returned, or {@code null} for any.
* @return The attribute value, or {@code null} if none or unparseable.
*
* @see MetadataNodeAccessor#setAttribute(String, Unit)
*
* @since 3.07
*/
@SuppressWarnings("unchecked")
public <Q extends Quantity<Q>> Unit<Q> getAttributeAsUnit(final String attribute, final Class<Q> quantity) {
String value = getAttribute(attribute);
if (value != null) try {
final Unit<?> unit = Units.valueOf(value);
if (quantity == null) {
return (Unit<Q>) unit;
}
try {
return unit.asType(quantity);
} catch (ClassCastException e) {
warning("getAttributeAsUnit", Errors.Keys.IncompatibleUnit_1, unit);
}
} catch (IllegalArgumentException e) {
warning(null, MetadataNodeParser.class, "getAttributeAsUnit", e);
}
return null;
}
/**
* Returns an attribute as a citation for the {@linkplain #selectChild selected element},
* or {@code null} if none.
*
* @param attribute The attribute to fetch (e.g. {@code "authority"}).
* @return The attribute value, or {@code null} if none.
*
* @see MetadataNodeAccessor#setAttribute(String, Citation)
*
* @since 3.06
*/
public Citation getAttributeAsCitation(final String attribute) {
return Citations.fromName(getAttribute(attribute));
}
/**
* Implementation of methods that parse a list.
* Example: {@link #getAttributeAsIntegers}, {@link #getAttributeAsDoubles}.
*
* @param sequence
* The character sequence to parse.
* @param type
* {@code Integer.TYPE} for parsing as {@code int},
* {@code Double.TYPE} for parsing as {@code double} or
* {@code String.class} for parsing as {@link String}.
* @param unique
* {@code true} if duplicated values should be collapsed into unique values, or
* {@code false} for preserving duplicated values.
* @param caller
* The method to report as the caller when logging warnings, or {@code null}
* for throwing exceptions instead than logging warnings.
* @return The attribute values, or {@code null} if none.
* @throws NumberFormatException if {@code logFailures} if {@code false} and an exception
* occurred while parsing a number.
*/
private Object parseSequence(final String sequence, final Class<?> type, final boolean unique,
final String caller) throws NumberFormatException
{
if (sequence == null) {
return null;
}
final Collection<Object> values;
if (unique) {
values = new LinkedHashSet<>();
} else {
values = new ArrayList<>();
}
final Class<?> wrapperType = Numbers.primitiveToWrapper(type);
final StringTokenizer tokens = new StringTokenizer(sequence);
while (tokens.hasMoreTokens()) {
final String token = tokens.nextToken().replace(NBSP, ' ').trim();
final Object value;
try {
value = Numbers.valueOf(token, wrapperType);
} catch (NumberFormatException e) {
if (caller == null) {
throw e;
}
warning(caller, Errors.Keys.UnparsableNumber_1, token);
continue;
}
values.add(value);
}
int count = 0;
final Object array = Array.newInstance(type, values.size());
for (final Object n : values) {
Array.set(array, count++, n);
}
assert Array.getLength(array) == count;
return array;
}
/**
* Convenience flavor of {@link #warning(String, int, Object)} with two arguments.
* We do not use the "variable argument list" syntax because of possible confusion
* with the {@code Object} type, which is too generic.
*/
private void warning(final String method, final short key, final Object arg1, final Object arg2) {
if (!Level.OFF.equals(warningLevel)) {
warning(method, key, new Object[] {arg1, arg2});
}
}
/**
* Convenience method for logging a warning. Do not allow overriding, because it
* would not work for warnings emitted by the {@link #getAttributeAsDate} method.
*/
private void warning(final String method, final short key, final Object value) {
if (!Level.OFF.equals(warningLevel)) {
warning(MetadataNodeParser.class, method, getErrorResources(), key, value);
}
}
/**
* Convenience method for logging a warning from a caller which may be outside this
* {@code MetadataNodeParser} class.
*/
final void warning(final Class<?> classe, final String method,
final IndexedResourceBundle resource, final short key, final Object value)
{
final Level warningLevel = this.warningLevel;
if (!Level.OFF.equals(warningLevel)) {
final LogRecord record = resource.getLogRecord(warningLevel, key, value);
record.setSourceClassName(classe.getName());
record.setSourceMethodName(method);
warningOccurred(record);
}
}
/**
* Convenience flavor of {@link #warning(String, int, Object)} with a message fetched
* from the given exception. This is invoked when we failed to parse an attribute.
* <p>
* We put the name of the exception class in the message only if the exception does
* not provide a localized message, or that message is made of only one word.
*
* @param level The maximal logging level to use, or {@code null} if none.
*/
final void warning(Level level, final Class<?> classe, final String method, final Exception exception) {
final Level warningLevel = this.warningLevel;
if (!Level.OFF.equals(warningLevel)) {
if (level == null || (warningLevel != null && warningLevel.intValue() < level.intValue())) {
level = warningLevel;
}
Warnings.log(this, level, classe, method, exception);
}
}
/**
* Invoked when a warning occurred. This method is invoked when some inconsistency
* has been detected in the spatial metadata. The default implementation tries to
* send the message to the warning listeners, if possible. The record is actually
* logged only if no listener can be reached.
* <p>
* More specifically, the typical chain of method calls is as below. Note that the
* actual chain may be different since any of those methods can be overridden, and
* the {@code ImageReader} can be an {@code ImageWriter} instead.
* <p>
* <ol>
* <li>{@code MetadataNodeParser.warningOccurred(LogRecord)}</li>
* <li>{@link SpatialMetadata#warningOccurred(LogRecord)}</li>
* <li>{@link org.geotoolkit.image.io.SpatialImageReader#warningOccurred(LogRecord)}</li>
* <li>{@link ImageReader#processWarningOccurred(String)}</li>
* <li>{@link javax.imageio.event.IIOReadWarningListener#warningOccurred(ImageReader, String)}</li>
* </ol>
*
* @param record The logging record to log.
* @return {@code true} if the message has been sent to at least one warning listener,
* or {@code false} otherwise (either the message has been sent to the logging
* system as a fallback, or the {@linkplain #getWarningLevel() warning level}
* if {@link Level#OFF OFF}).
*/
@Override
public boolean warningOccurred(final LogRecord record) {
if (!Level.OFF.equals(warningLevel)) {
return Warnings.log(metadata, record);
}
return false;
}
/**
* Returns the level at which warnings are emitted, or {@link Level#OFF} if they are disabled.
* The default value is {@link Level#WARNING}.
* <p>
* Note that the warnings are effectively sent to the logging framework only if there is
* no registered Image I/O warning listeners. See the {@link #warningOccurred(LogRecord)}
* javadoc for details.
*
* @return The current level at which warnings are emitted.
*/
public Level getWarningLevel() {
return warningLevel;
}
/**
* Sets the warning level, or disable warnings. By default, warnings are enabled and set to
* the {@link Level#WARNING WARNING} level. Subclasses way want to temporarily disable the
* warnings (using the {@link Level#OFF} argument value) when failures are expected as the
* normal behavior. For example a subclass may invoke {@link #getAttributeAsInteger(String)}
* and fallback on {@link #getAttributeAsDouble(String)} if the former failed. In such case,
* the warnings should be disabled for the integer parsing, but not for the floating point
* parsing.
* <p>
* Note that a low warning level like {@link Level#FINE} may prevent the warnings to
* be sent to the console logger, but does not prevent the warnings to be sent to the
* Image I/O warning listeners (see {@link #warningOccurred(LogRecord)} javadoc). Only
* {@link Level#OFF} really disables warnings.
*
* @param level {@link Level#OFF} for disabling warnings, or an other value for enabling them.
* @return The previous state before this method has been invoked.
*/
public Level setWarningLevel(final Level level) {
ensureNonNull("level", level);
final Level old = warningLevel;
warningLevel = level;
return old;
}
/**
* Returns the resources for formatting error messages.
*/
final IndexedResourceBundle getErrorResources() {
return Errors.getResources(getLocale());
}
/**
* Returns the locale to use for formatting warnings and error messages.
* This method delegates to {@link SpatialMetadata#getLocale()} if possible,
* or returns {@code null} otherwise.
*
* @return The locale to use for formatting the warnings, or {@code null}.
*
* @since 3.07
*/
@Override
public Locale getLocale() {
return (metadata instanceof Localized) ? ((Localized) metadata).getLocale() : null;
}
/**
* Returns a string representation of the wrapped {@link IIOMetadata} as a tree. The root of
* the tree contains the class of this accessor and the value defined in the {@link #name()}
* javadoc. Attributes are leafs formatted as <var>key</var>="<var>value</var>", while elements
* and child branches.
* <p>
* This method is useful for visual check of the {@link IIOMetadata} content and should be
* used only for debugging purpose. Note that the output may span many lines.
*/
@Override
public String toString() {
return toString(getClass());
}
/**
* Implementation of {@link #toString()} using the name of the given class for formatting
* the root.
*/
final String toString(final Class<?> owner) {
final StringBuilder buffer = new StringBuilder(Classes.getShortName(owner)).append("[\"");
int offset = buffer.length();
buffer.append(Trees.toString(Trees.xmlToSwing(parent)));
offset = buffer.indexOf(System.lineSeparator(), offset); // Should never be -1.
return buffer.insert(offset, "\"]").toString();
}
}