/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2009-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.util.Map;
import java.util.HashMap;
import java.util.Locale;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormat;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.apache.sis.util.Localized;
import org.geotoolkit.gui.swing.tree.Trees;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* A view of an {@link IIOMetadata} instance as a tree table. The tree structure is determined by
* an {@link IIOMetadataFormat}, which must be provided to the constructor. After the construction,
* different instances of {@code IIOMetadata} can be given to this {@code MetadataTreeTable} in
* order to generate tables with different values. If no {@code IIOMetadata} instance is given,
* then this object represents only the structure of the format with its restrictions (expected
* type, range of values, <i>etc.</i>) but no values.
* <p>
* The root of the tree is obtained by {@link #getRootNode()}. The table contains at most
* {@value #COLUMN_COUNT} columns, described below:
* <ol>
* <li>A human-readable name of the nodes.</li>
* <li>A description of the node.</li>
* <li>The {@linkplain Class#getSimpleName() simple class names} of values.</li>
* <li>The range of occurrences (how many time the node can be repeated).</li>
* <li>The node value (this column is omitted if the tree is for
* {@link IIOMetadataFormat} instead than {@link IIOMetadata}).</li>
* <li>The default value.</li>
* <li>A description of valid values (either as a range or as an enumeration).</li>
* </ol>
* <p>
* This class works with arbitrary implementations of {@code IIOMetadata};
* it doesn't need to be the specialized implementations defined in Geotk.
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.16
*
* @see org.geotoolkit.gui.swing.image.IIOMetadataPanel
*
* @since 3.04
* @module
*/
public class MetadataTreeTable implements Localized {
/**
* The number of columns in the table ({@value}), when every columns are present. Note that
* the {@linkplain #VALUE_COLUMN value column} is omitted if this {@code MetadataTreeTable}
* is given only an {@link IIOMetadataFormat} without any {@link IIOMetadata} instance for
* providing the actual values.
*/
public static final int COLUMN_COUNT = 7;
/**
* The column which contains the values. This is the column which is omitted if this
* table describe only an {@link IIOMetadataFormat} without {@link IIOMetadata}.
*
* @since 3.05
*/
public static final int VALUE_COLUMN = 4;
/**
* The expected format of {@code IIOMetadata} instances.
*/
final IIOMetadataFormat format;
/**
* The current metadata, or {@code null} if none.
*/
private IIOMetadata metadata;
/**
* The Locale for which localization will be attempted.
*/
private Locale locale;
/**
* {@code true} if the tree returned by {@link #getRootNode()} is allowed to prune empty
* nodes and merge singleton attributes with the parent node. This parameter has no effect
* if this {@code MetatataTreeTable} is used for an {@link IIOMetadataFormat} only.
*/
private boolean simplificationAllowed;
/**
* The root of the tree table. This is the result of {@link #getRootNode()}, which use all
* the above fields. It will be created only when first needed and reset to {@code null} if
* one of the above field change, in order to force the creation of a new tree.
*/
private transient MetadataTreeNode tree;
/**
* Creates a new metadata tree for the given format.
*
* @param format The expected format of {@code IIOMetadata} instances.
*/
public MetadataTreeTable(final IIOMetadataFormat format) {
ensureNonNull("format", format);
this.format = format;
locale = Locale.getDefault(Locale.Category.DISPLAY);
}
/**
* Returns the metadata format specified at construction time.
*
* @return The metadata format.
*
* @since 3.05
*/
public IIOMetadataFormat getMetadataFormat() {
return format;
}
/**
* Returns the metadata to be formatted as a tree table.
*
* @return The current metadata, or {@code null} if none.
*/
public IIOMetadata getMetadata() {
return metadata;
}
/**
* Sets the metadata to be formatted as a tree table. This method invalidates any
* {@linkplain #getRootNode() root node} obtained before the call to this method.
*
* @param metadata The new metadata, or {@code null} if none.
* @throws IllegalArgumentException If the given metadata does not support
* the format given to the {@code MetadataTreeTable} constructor.
*/
public void setMetadata(final IIOMetadata metadata) throws IllegalArgumentException {
this.metadata = metadata;
tree = null; // Will force new calculation.
}
/**
* Returns the locale for which localization will be attempted.
*
* @return The locale for which localization will be attempted.
*/
@Override
public Locale getLocale() {
return locale;
}
/**
* Sets the locale for which localization will be attempted. This change applies to
* future {@link MetadataTreeNode} instances to be created by {@link #getRootNode()}.
* The effect on previous instances (if any) is undefined - some will take the change
* in account, other will ignore.
*
* @param locale The locale for which localization will be attempted.
*/
public void setLocale(final Locale locale) {
ensureNonNull("locale", locale);
if (!locale.equals(this.locale)) {
this.locale = locale;
tree = null; // Will force new calculation.
}
}
/**
* Returns {@code true} if the tree returned by {@link #getRootNode()} can be simplified.
* Simplification are convenient for GUI purpose, but usually not appropriate for
* programmatic purpose. The simplifications, if allowed, are:
* <p>
* <ul>
* <li>If a node has only one attribute, do not add that attribute to the tree.
* Instead, move its {@linkplain MetadataTreeNode#getUserObject() value} in
* the parent element.</li>
* <li>Prune empty nodes.</li>
* </ul>
* </p>
* The default value is {@code false}.
*
* @return {@code true} if the tree can be simplified.
*
* @since 3.05
*/
public boolean getSimplificationAllowed() {
return simplificationAllowed;
}
/**
* Sets whatever the tree returned by {@link #getRootNode()} can be simplified. This
* parameter is ignored if there is no {@link IIOMetadata} instance associated with
* this {@code MetadataTreeTable}.
*
* @param allowed {@code true} if the tree can be simplified.
*
* @since 3.05
*/
public void setSimplificationAllowed(final boolean allowed) {
if (allowed != simplificationAllowed) {
simplificationAllowed = allowed;
tree = null; // Will force new calculation.
}
}
/**
* Returns the root of the Tree Table representation of the metadata. If there is
* no {@link IIOMetadata} instance currently set, then returns a representation of
* the {@link IIOMetadataFormat}.
*
* @return The root of a tree representation of the metadata.
*/
public MetadataTreeNode getRootNode() {
if (tree == null) {
final boolean hasValue = (metadata != null);
final Node xmlNode = hasValue ? metadata.getAsTree(format.getRootName()) : null;
final MetadataTreeNode root = new MetadataTreeNode(this, format.getRootName(), xmlNode, hasValue);
addChilds(root, new HashMap<String,Object>());
tree = root;
}
return tree;
}
/**
* Adds attributes and child elements to the given parent.
* This method invokes itself recursively.
*
* @param addTo The parent into which the childs need to be added.
* @param done A safety against infinite recursivity. The keys are the name of childs
* in the process of being added. The value are the node for which t
* @return {@code true} if at least one element or attribute has been added.
*/
private boolean addChilds(final MetadataTreeNode addTo, final Map<String,Object> done) {
boolean added = false;
final boolean includeEmpty = !addTo.hasValue || !simplificationAllowed;
/*
* Adds the attributes first. They will be children of the parent node.
*/
final String name = addTo.getName();
String[] childs = format.getAttributeNames(name);
if (childs != null) {
for (final String childName : childs) {
final MetadataTreeNode child = new MetadataTreeNode(addTo, childName);
if (includeEmpty || child.getUserObject() != null) {
added = true;
if (simplificationAllowed && childs.length == 1) {
/*
* Attempt a simplification as documented in the getSimplificationAllowed()
* method. If the simplification succeed, do not add the attribute as a child.
*/
if (child.copyToParent(addTo)) {
break;
}
}
addTo.add(child);
}
}
}
/*
* Adds the child elements after the attributes. This method does not verify if the
* elements are childs of this node; they could be childs of an unrelated node.
* However the IIOMetadataFormat API is defined in such a way that we shall not
* define different elements with the same name.
*/
childs = format.getChildNames(name);
if (childs != null) {
for (final String childName : childs) {
if (childName != null) {
/*
* If there is some values associated to the metadata, count the number of
* occurrence of the child element (may be greater than 1 if the policy is
* CHILD_POLICY_REPEAT). Otherwise (if we are formatting the metadata format
* instead than the values) add the child exactly once.
*/
int count = 1;
NodeList elements = null;
if (addTo.xmlNode instanceof Element) {
elements = ((Element) addTo.xmlNode).getElementsByTagName(childName);
if (elements != null) {
count = elements.getLength();
}
}
for (int i=0; i<count; i++) {
final Node xmlChild = (elements != null) ? elements.item(i) : null;
/*
* Check for recursive invocation with the same child name. I'm not sure
* if this is legal, but it happen with the TIFF format provided by JAI.
* If the child to add is already in the process of being added, we will
* skip the second occurrence.
*/
final Object newNode = (xmlChild != null) ? xmlChild : Void.TYPE;
final Object oldNode = done.put(childName, newNode);
if (newNode != oldNode) {
final MetadataTreeNode child = new MetadataTreeNode(this, childName, xmlChild, addTo.hasValue);
if (addChilds(child, done) || includeEmpty || child.getUserObject() != null) {
addTo.add(child);
added = true;
}
}
if ((oldNode != null ? done.put(childName, oldNode) : done.remove(childName)) != newNode) {
throw new AssertionError(childName);
}
}
}
}
}
return added;
}
/**
* Returns a string representation of this tree table. The current implementation
* formats a tree, but it could change in future Geotk version. Consequently this
* method should be used for debugging purpose only.
*
* @since 3.16
*/
@Override
public String toString() {
return Trees.toString(getRootNode());
}
}