/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2010-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2010-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.internal.image.io;
import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOMetadataFormat;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.NamedNodeMap;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.gui.swing.tree.Trees;
import org.geotoolkit.image.io.metadata.MetadataTreeTable;
import static org.apache.sis.util.collection.Containers.hashMapCapacity;
/**
* A metadata format that describe an existing tree of metadata. The element names and attribute
* names are extracted from the node names. The type of attribute node ({@link #DATATYPE_STRING},
* {@link #DATATYPE_DOUBLE}, <i>etc.</i>) can optionally be stored.
* <p>
* Subclasses can override the {@link #getDataType(Node, int)} method in order to specify the
* {@code DATATYPE_*} constant to associate to an attribute.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.08
*
* @since 3.08 (derived from 2.4)
* @module
*/
public abstract class SampleMetadataFormat implements IIOMetadataFormat {
/**
* The name of the root element of the format.
*/
private final String rootName;
/**
* The elements, inferred from the nodes.
* Will be created when first needed.
*/
private Map<String, Element> elements;
/**
* A description of an element. the inherited {@code HashMap} is the set of attributes
* (keys), where the values are the data type.
*/
@SuppressWarnings("serial")
private static final class Element extends LinkedHashMap<String,Integer> {
/**
* The name of child elements, or {@code null} if none.
*/
String[] childs;
}
/**
* Creates a new {@code SampleMetadataFormat} instance.
*
* @param rootName The name of the root node.
*/
public SampleMetadataFormat(final String rootName) {
this.rootName = rootName;
}
/**
* Returns the root node of metadata data. This will be invoked when first needed
* for inferring the metadata format.
*
* @return The root node of metadata.
*/
protected abstract Node getDataRootNode();
/**
* Adds the given node and all its children. This method is first invoked by
* the constructor, then invokes itself recursively for building the tree.
*
* @param node The root of the tree to append.
* @return The name of the given node.
*/
private String addNode(final Node node) {
final String name = node.getNodeName();
Element element = elements.get(name);
if (element == null) {
element = new Element();
elements.put(name, element);
}
/*
* Add the attributes.
*/
final NamedNodeMap attributes = node.getAttributes();
if (attributes != null) {
final int length = attributes.getLength();
for (int i=0; i<length; i++) {
final Node attr = attributes.item(i);
final String attrName = attr.getNodeName();
final int type = getDataType(attr, i);
final Integer old = element.put(attrName, type);
if (old != null && old.intValue() != type) {
// In case of mismatch, use the most generic type.
element.put(attrName, DATATYPE_STRING);
}
}
}
/*
* Add the child elements.
*/
final NodeList childs = node.getChildNodes();
if (childs != null) {
final int length = childs.getLength();
if (length != 0) {
final Set<String> childNames;
if (element.childs != null) {
childNames = new LinkedHashSet<>(Arrays.asList(element.childs));
} else {
childNames = new LinkedHashSet<>(hashMapCapacity(length));
}
for (int i=0; i<length; i++) {
childNames.add(addNode(childs.item(i)));
}
element.childs = childNames.toArray(new String[childNames.size()]);
}
}
return name;
}
/**
* Returns the element of the given name. The metadata format will be created
* when this method is first invoked.
*
* @throws IllegalArgumentException If there is no element for the given name.
*/
private synchronized Element getElement(final String elementName) throws IllegalArgumentException {
if (elements == null) {
elements = new LinkedHashMap<>();
addNode(getDataRootNode());
}
final Element element = elements.get(elementName);
if (element == null) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.NoSuchElementName_1, elementName));
}
return element;
}
/**
* Invoked by the constructor for determining the data type of the given attribute.
* The default implementation returns {@link #DATATYPE_STRING} in all cases.
* Subclasses can override this method if they can determine a more accurate type.
*
* @param attribute The attribute node.
* @param index The attribute index in the parent element.
* @return One of the {@code DATATYPE_*} constants.
*/
protected int getDataType(final Node attribute, final int index) {
return DATATYPE_STRING;
}
/**
* Returns the name of the root element of the format.
*/
@Override
public String getRootName() {
return rootName;
}
/**
* Returns {@code true} if the given element is allowed to appear.
* The default implementation returns unconditionally {@code true}.
*/
@Override
public boolean canNodeAppear(final String elementName, final ImageTypeSpecifier imageType) {
return true;
}
/**
* Returns the minimum number of children.
* The default implementation returns {@code 0} (no minimum).
*/
@Override
public int getElementMinChildren(final String elementName) {
return 0;
}
/**
* Returns the minimum number of children.
* The default implementation returns {@code MAX_VALUE} (no maximum).
*/
@Override
public int getElementMaxChildren(final String elementName) {
return Integer.MAX_VALUE;
}
/**
* Returns a description of the element.
* The default implementation returns {@code null} (no description).
*/
@Override
public String getElementDescription(final String elementName, final Locale locale) {
return null;
}
/**
* Returns the legal pattern of children.
* The default implementation returns {@link #CHILD_POLICY_SOME}.
*/
@Override
public int getChildPolicy(final String elementName) {
return CHILD_POLICY_SOME;
}
/**
* Returns the names of the element which are allowed to be children of the named element.
*/
@Override
public String[] getChildNames(final String elementName) {
final Element element = getElement(elementName);
return (element.childs != null) ? element.childs.clone() : null;
}
/**
* Returns the names of the attributes that may be associated with the named element.
*/
@Override
public String[] getAttributeNames(final String elementName) {
final Set<String> attributes = getElement(elementName).keySet();
return attributes.toArray(new String[attributes.size()]);
}
/**
* Returns whether the values of the given attribute within the named element are arbitrary.
* The default implementation returns {@link #VALUE_ARBITRARY}.
*/
@Override
public int getAttributeValueType(final String elementName, final String attrName) {
return VALUE_ARBITRARY;
}
/**
* Returns the format and interpretation of the value of the given attribute within the
* named element.
*/
@Override
public int getAttributeDataType(final String elementName, final String attrName) {
final Integer type = getElement(elementName).get(attrName);
if (type == null) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.NoSuchAttribute_1, attrName));
}
return type;
}
/**
* Returns true if the named attribute must be present within the named element.
* The default implementation returns {@code false}.
*/
@Override
public boolean isAttributeRequired(final String elementName, final String attrName) {
return false;
}
/**
* Returns the default value of the named attribute.
* The default implementation returns {@code null}.
*/
@Override
public String getAttributeDefaultValue(final String elementName, final String attrName) {
return null;
}
/**
* Returns the legal enumerated values for the given attribute within the named element.
* The default implementation throws {@link IllegalArgumentException} since no attribute
* are enumerated values.
*/
@Override
public String[] getAttributeEnumerations(final String elementName, final String attrName) {
throw new IllegalArgumentException();
}
/**
* Returns the minimum legal value for the attribute. The default implementation
* throws {@link IllegalArgumentException} since no attribute are defined as range.
*/
@Override
public String getAttributeMinValue(final String elementName, final String attrName) {
throw new IllegalArgumentException();
}
/**
* Returns the maximum legal value for the attribute. The default implementation
* throws {@link IllegalArgumentException} since no attribute are defined as range.
*/
@Override
public String getAttributeMaxValue(final String elementName, final String attrName) {
throw new IllegalArgumentException();
}
/**
* Returns the minimum number of items in the attribute.
* The default implementation returns {@code 0} (no minimum).
*/
@Override
public int getAttributeListMinLength(final String elementName, final String attrName) {
return 0;
}
/**
* Returns the maximum number of items in the attribute.
* The default implementation returns {@code MAX_VALUE} (no maximum).
*/
@Override
public int getAttributeListMaxLength(final String elementName, final String attrName) {
return Integer.MAX_VALUE;
}
/**
* Returns a description of the named attribute.
* The default implementation returns {@code null}.
*/
@Override
public String getAttributeDescription(final String elementName, final String attrName, final Locale locale) {
return null;
}
/**
* Returns the type of values (enumeration, range, or array) that are allowed for the
* object reference. The default implementation returns {@link #VALUE_NONE}.
*/
@Override
public int getObjectValueType(final String elementName) {
return VALUE_NONE;
}
/**
* Returns the type of the object reference stored within the element.
* The default implementation throws {@link IllegalArgumentException}.
*/
@Override
public Class<?> getObjectClass(final String elementName) {
throw new IllegalArgumentException();
}
/**
* Returns the default value for the object reference within the named element.
* The default implementation returns {@code null}.
*/
@Override
public Object getObjectDefaultValue(final String elementName) {
return null;
}
/**
* Returns the legal enumeration for the object reference within the named element.
* The default implementation throws {@link IllegalArgumentException}.
*/
@Override
public Object[] getObjectEnumerations(final String elementName) {
throw new IllegalArgumentException();
}
/**
* Returns the minimal for the object reference within the named element.
* The default implementation throws {@link IllegalArgumentException}.
*/
@Override
public Comparable<?> getObjectMinValue(String elementName) {
throw new IllegalArgumentException();
}
/**
* Returns the minimal for the object reference within the named element.
* The default implementation throws {@link IllegalArgumentException}.
*/
@Override
public Comparable<?> getObjectMaxValue(final String elementName) {
throw new IllegalArgumentException();
}
/**
* Returns the minimum number of items in the object reference.
* The default implementation returns {@code 0} (no minimum).
*/
@Override
public int getObjectArrayMinLength(final String elementName) {
return 0;
}
/**
* Returns the maximum number of items in the object reference.
* The default implementation returns {@code MAX_VALUE} (no maximum).
*/
@Override
public int getObjectArrayMaxLength(final String elementName) {
return Integer.MAX_VALUE;
}
/**
* Returns a string representation of this format.
* The default implementation formats this object as a tree.
*/
@Override
public String toString() {
return Trees.toString(new MetadataTreeTable(this).getRootNode());
}
}