/*
 *    GeoTools - OpenSource mapping toolkit
 *    http://geotools.org
 *    (C) 2007, GeoTools Project Managment Committee (PMC)
 *    (C) 2007, 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.geotools.image.io.metadata;

// J2SE dependencies
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import javax.imageio.metadata.IIOMetadataNode;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

// Geotools dependencies
import org.geotools.resources.XMath;
import org.geotools.resources.Utilities;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.OptionalDependencies;
import org.geotools.util.UnsupportedImplementationException;


/**
 * Base class for {@linkplain GeographicMetadata geographic 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 "CoordinateReferenceSystem/Datum"}</li>
 *   <li>{@code "CoordinateReferenceSystem/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 "CoordinateReferenceSystem/CoordinateSystem/Axis"}</li>
 *   <li>{@code "GridGeometry/Envelope/CoordinateValues"}</li>
 *   <li>{@code "SampleDimensions/SampleDimension"}</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,
 *         "CoordinateReferenceSystem/CoordinateSystem", "Axis");
 *
 * accessor.selectParent();
 * String csName = accessor.getString("name");
 *
 * accessor.selectChild(0);
 * String firstAxisName = accessor.getString("name");
 * </pre></blockquote>
 *
 * @since 2.4
 * @version $Id: MetadataAccessor.java 27583 2007-10-23 11:29:26Z desruisseaux $
 * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/metadata/MetadataAccessor.java $
 * @author Martin Desruisseaux
 */
public class MetadataAccessor {
    /**
     * The separator between names in a node path.
     */
    private static final char SEPARATOR = '/';

    /**
     * The owner of this accessor.
     */
    private final GeographicMetadata 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;

    /**
     * {@code true} if warnings are enabled.
     */
    private transient boolean warningsEnabled = true;

    /**
     * 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 GeographicMetadata metadata, final String parentPath, final String childPath) {
        this.metadata = metadata;
        final Node root = metadata.getRootNode();
        /*
         * Fetchs 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 childs = new ArrayList(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, new Integer(count)});
                    // Fall through for picking the first node.
                }
                case 1: {
                    parent = (Node) 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.EMPTY_LIST;
        }
        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 UnsupportedImplementationException(child.getClass());
        }
    }

    /**
     * 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 Object /*T*/ getUserObject(Class/*<T>*/ type) throws ClassCastException {
        type = XMath.primitiveToWrapper(type);
        Object value = getUserObject();
        if (value instanceof CharSequence) {
            if (Number.class.isAssignableFrom(type)) {
                value = XMath.valueOf(type, value.toString());
            } else {
                final Class component = XMath.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 value; // TODO: use type.cast with Java 5.
    }

    /**
     * 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 UnsupportedImplementationException if the selected element is not an instance of
     *         {@link IIOMetadataNode}.
     *
     * @see #getUserObject()
     */
    protected void setUserObject(final Object value) throws UnsupportedImplementationException {
        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 UnsupportedImplementationException(Errors.format(ErrorKeys.ILLEGAL_CLASS_$2,
                    Utilities.getShortClassName(element), Utilities.getShortName(IIOMetadataNode.class)));
        }
        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 CoordinateReferenceSystem.
     */
    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) {
                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) {
            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();
        } else {
            numbers = new ArrayList();
        }
        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) {
                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 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 StringBuffer buffer = new StringBuffer();
            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;
    }

    /**
     * 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.
     */
    protected Date getDate(final String attribute) {
        String value = getString(attribute);
        if (value != null) {
            value = trimFractionalPart(value);
            // TODO: remove the cast with J2SE 1.5.
            return (Date) metadata.dateFormat().parse(value);
        }
        return null;
    }

    /**
     * Set 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.
     */
    protected void setDate(final String attribute, final Date value) {
        String text = null;
        if (value != null) {
            text = metadata.dateFormat().format(value);
        }
        setString(attribute, text);
    }

    /**
     * Trims the factional 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;
    }

    /**
     * Convenience method for logging a warning. Do not allow overriding, because
     * it would not work for warnings emitted by the {@link #getDate} method.
     */
    final void warning(final String method, final int key, final Object value) {
        if (warningsEnabled) {
            final LogRecord record = Errors.getResources(metadata.getLocale()).
                    getLogRecord(Level.WARNING, key, value);
            record.setSourceClassName(MetadataAccessor.class.getName());
            record.setSourceMethodName(method);
            warningOccurred(record);
        }
    }

    /**
     * Invoked when a warning occured. This method is invoked when some inconsistency has
     * been detected in the geographic metadata. The default implementation delegates
     * to {@link GeographicMetadata#warningOccurred}.
     */
    protected void warningOccurred(final LogRecord record) {
        if (warningsEnabled) {
            metadata.warningOccurred(record);
        }
    }

    /**
     * Enables or disables the warnings. Warnings are enabled by default. Subclasses way want
     * to temporarily disable the warnings when failures are expected as the normal behavior.
     * For example a subclass may invokes {@link #getInteger} and fallbacks on {@link #getDouble}
     * if the former failed. In such case, the warnings should be disabled for the integer parsing,
     * but not for the floating point parsing.
     *
     * @param  enabled {@code true} for enabling warnings, or {@code false} for disabling.
     * @return The previous state before this method has been invoked.
     */
    protected boolean setWarningsEnabled(final boolean enabled) {
        final boolean old = warningsEnabled;
        warningsEnabled = enabled;
        return old;
    }

    /**
     * Returns a string representation of metadata, mostly for debugging purpose.
     */
    public String toString() {
        return OptionalDependencies.toString(OptionalDependencies.xmlToSwing(parent));
    }
}
