/*
* 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.Set;
import java.util.Locale;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import javax.imageio.metadata.IIOMetadataFormat;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.apache.sis.util.CharSequences;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.Classes;
import org.apache.sis.util.Numbers;
import org.apache.sis.util.ObjectConverters;
import org.apache.sis.util.UnconvertibleObjectException;
import org.geotoolkit.metadata.ValueRestriction;
import org.geotoolkit.gui.swing.tree.NamedTreeNode;
import org.geotoolkit.gui.swing.tree.TreeTableNode;
import org.geotoolkit.resources.Errors;
import static org.geotoolkit.image.io.metadata.MetadataTreeTable.COLUMN_COUNT;
import static org.geotoolkit.image.io.metadata.MetadataTreeTable.VALUE_COLUMN;
/**
* A node in the tree produced by {@link MetadataTreeTable}. The value returned by the
* {@link #toString() toString()} method is the programmatic name of the element or attribute
* represented by this node. The values returned by the {@link #getValueAt(int)} method are
* the values for the columns documented in the {@link MetadataTreeTable} javadoc. Those
* values are also accessible by specific getter methods:
* <p>
* <ol>
* <li>{@link #getLabel()}</li>
* <li>{@link #getDescription()}</li>
* <li>{@link #getValueType()}</li>
* <li>{@link #getOccurrences()}</li>
* <li>{@link #getUserObject()} (this column may be omitted - see {@link MetadataTreeTable})</li>
* <li>{@link #getDefaultValue()}</li>
* <li>{@link #getValueRestriction()}</li>
* </ol>
* <p>
* By default the value returned by {@link #getAllowsChildren() getAllowsChildren()} is:
* <p>
* <ul>
* <li>{@code true} if the node is an element.</li>
* <li>{@code false} if the node is an attribute.</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
* @version 3.05
*
* @since 3.05
* @module
*/
public final class MetadataTreeNode extends NamedTreeNode implements TreeTableNode {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 3458235875074371134L;
/**
* The tree which is the owner of this node.
* This is not allowed to be null.
*/
private final MetadataTreeTable tree;
/**
* The name of the element to be represented as a node.
* This field is not allowed to be {@code null}.
*/
private final String element;
/**
* If this node is actually an attribute that belong to the above element,
* the name of that attribute. Otherwise {@code null}.
*/
private final String attribute;
/**
* If this {@code MetadataTreeNode} is associated with a XML tree node, that node.
* Otherwise {@code null}. This is non-null only if an {@link IIOMetadata} instance
* has been given to the {@link MetadataTreeTable}.
* <p>
* Many instances of {@link MetadataTreeNode} with identical {@link #tree}, {@link #element}
* and {@link #attribute} values but different {@code xmlNode} may occur if:
* <p>
* <ul>
* <li>An {@link IIOMetadata} instance have been assigned to {@link MetadataTreeTable}.</li>
* <li>The child policy for this node is {@link IIOMetadataFormat#CHILD_POLICY_REPEAT}.</li>
* </ul>
* <p>
* <b>Example:</b> enumerating the RGB entries in a color palette.
*/
final Node xmlNode;
/**
* {@code true} if the tree that contains this node is expected to have value. Note that
* this boolean may be {@code true} even if {@code xmlNode} is {@code null}, because the
* values can be in other nodes.
*/
final boolean hasValue;
/**
* The label to show in the first column.
*/
private transient String label;
/**
* The description. Will be fetched only when first needed.
*/
private transient String description;
/**
* The base type of values, or {@code null} if not yet determined. If the value type is a
* collection, then it will be represented as an array type. If the node is not allowed to
* store any object, then this method returns {@link Void#TYPE}.
*/
private transient Class<?> valueType;
/**
* The minimum and maximum occurrences of children in this node,
* or {@code null} if not yet determined.
*/
private transient NumberRange<Integer> occurrences;
/**
* The valid values as a range or a comma-separated list, or {@code null} if not yet
* determined. If we have determined that there is no list of valid values, then this
* will be set to {@link ValidValues#UNRESTRICTED}.
*/
private transient ValueRestriction validValues;
/**
* The default value, or {@code null} if not yet determined. If we have determined
* that there is no default value, then this will be set to an empty string.
*/
private transient Object defaultValue;
/**
* Creates a new node for a metadata element.
*
* @param tree The tree which is the owner of this node.
* @param element The element name.
* @param node The XML node, or {@code null}.
* @param hasValue {@code true} if the tree that contains this node has a value column.
*/
MetadataTreeNode(final MetadataTreeTable tree, final String element,
final Node xmlNode, final boolean hasValue)
{
super(element);
this.tree = tree;
this.element = element;
this.attribute = null;
this.xmlNode = xmlNode;
this.hasValue = hasValue;
setAllowsChildren(true);
}
/**
* Creates a new node for a metadata attribute.
*
* @param element The element which own the attribute.
* @param attribute The attribute name.
*/
MetadataTreeNode(final MetadataTreeNode element, final String attribute) {
super(attribute);
this.tree = element.tree;
this.element = element.element;
this.attribute = attribute;
this.xmlNode = element.xmlNode;
this.hasValue = element.hasValue;
setAllowsChildren(false);
}
/**
* Returns the parent of this node.
*
* @return The parent of this node, or {@code null} if none.
*/
@Override
public final MetadataTreeNode getParent() {
return (MetadataTreeNode) super.getParent();
}
/**
* Returns the programmatic name of this node.
* This is for example the UML identifier as defined ISO 19115.
*
* @return The programmatic name of this node.
*/
@Override
public final String getName() {
return super.toString();
}
/**
* Returns the display label. It will be constructed from the {@linkplain #getName
* programmatic name} (often the UML identifier) when first needed and cached for
* future reuse.
*
* @return The label inferred from the programmatic node name (never null).
*/
public String getLabel() {
if (label == null) {
label = CharSequences.camelCaseToSentence(getName()).toString();
}
return label;
}
/**
* Returns the description, or {@code null} if none. The description will be localized
* in the {@linkplain MetadataTreeTable#getLocale() Tree Table locale}, if possible.
*
* @return The description, or {@code null} if none.
*/
public String getDescription() {
String description = this.description;
if (description == null) {
final Locale locale = tree.getLocale();
final IIOMetadataFormat format = tree.format;
if (attribute == null){
description = format.getElementDescription(element, locale);
} else {
description = format.getAttributeDescription(element, attribute, locale);
}
if (description == null) {
description = "";
}
this.description = description;
}
return description.isEmpty() ? null : description;
}
/**
* Returns the range of occurrences that are valid for this node. This method never returns
* {@code null} since the {@linkplain NumberRange#getMinValue() minimum value} of occurrences
* is at least 0.
*
* @return The range of occurrences (never null).
*/
public NumberRange<Integer> getOccurrences() {
if (occurrences == null) {
Integer min=0, max=1;
final IIOMetadataFormat format = tree.format;
if (attribute == null) {
if (parent != null) {
final String parent = getParent().element;
switch (format.getChildPolicy(parent)) {
case IIOMetadataFormat.CHILD_POLICY_REPEAT: {
min = format.getElementMinChildren(parent);
max = format.getElementMaxChildren(parent);
break;
}
case IIOMetadataFormat.CHILD_POLICY_ALL: {
min = 1;
break;
}
case IIOMetadataFormat.CHILD_POLICY_EMPTY: {
max = 0;
break;
}
}
}
} else {
switch (format.getAttributeValueType(element, attribute)) {
case IIOMetadataFormat.VALUE_LIST: {
min = format.getAttributeListMinLength(element, attribute);
max = format.getAttributeListMaxLength(element, attribute);
break;
}
default: {
if (format.isAttributeRequired(element, attribute)) {
min = 1;
}
break;
}
}
}
// Consider MIN|MAX_VALUE as unbounded.
if (min == Integer.MIN_VALUE) min = null;
if (max == Integer.MAX_VALUE) max = null;
occurrences = new NumberRange<>(Integer.class, min, true, max, true);
}
return occurrences;
}
/**
* Returns the type of user object that can be associated to the element or attribute.
* {@link java.util.Collection} types are converted to array types. If the node is not
* allowed to store any object, then this method returns {@code null}.
*
* @return The type of user object, or {@code null} if this node does not allow value.
*/
@SuppressWarnings("fallthrough")
public Class<?> getValueType() {
Class<?> type = valueType;
if (type == null) {
type = Void.TYPE; // The default value.
boolean isArray = false;
final IIOMetadataFormat format = tree.format;
if (attribute == null) {
switch (format.getObjectValueType(element)) {
case IIOMetadataFormat.VALUE_NONE: break;
case IIOMetadataFormat.VALUE_LIST: isArray = true; // Fall through
default: type = format.getObjectClass(element); break;
}
} else {
switch (format.getAttributeValueType(element, attribute)) {
case IIOMetadataFormat.VALUE_LIST: isArray = true; break;
}
switch (format.getAttributeDataType(element, attribute)) {
case IIOMetadataFormat.DATATYPE_STRING: type = String.class; break;
case IIOMetadataFormat.DATATYPE_INTEGER: type = Integer.class; break;
case IIOMetadataFormat.DATATYPE_DOUBLE: type = Double.class; break;
case IIOMetadataFormat.DATATYPE_FLOAT: type = Float.class; break;
case IIOMetadataFormat.DATATYPE_BOOLEAN: type = Boolean.class; break;
}
}
if (isArray) {
type = Classes.changeArrayDimension(Numbers.wrapperToPrimitive(type), 1);
}
valueType = type;
}
return Void.TYPE.equals(type) ? null : type;
}
/**
* Returns the range or the enumeration of valid values. If there is no restriction
* on the valid values, then this method returns {@code null}.
*
* @return A description of the valid values, or {@code null} if none.
*/
public ValueRestriction getValueRestriction() {
ValueRestriction valids = validValues;
if (valids == null) {
valids = ValidValues.UNRESTRICTED; // Will be the default.
final IIOMetadataFormat format = tree.format;
if (attribute == null) {
final int type = format.getObjectValueType(element);
switch (type & ~(IIOMetadataFormat.VALUE_RANGE_MIN_INCLUSIVE_MASK |
IIOMetadataFormat.VALUE_RANGE_MAX_INCLUSIVE_MASK))
{
case IIOMetadataFormat.VALUE_RANGE: {
final Class<?> datatype = format.getObjectClass(element);
valids = ValidValues.range(datatype, type,
format.getObjectMinValue(element),
format.getObjectMaxValue(element));
break;
}
case IIOMetadataFormat.VALUE_ENUMERATION: {
valids = new ValidValues(format.getObjectEnumerations(element));
break;
}
}
} else {
final int type = format.getAttributeValueType(element, attribute);
switch (type & ~(IIOMetadataFormat.VALUE_RANGE_MIN_INCLUSIVE_MASK |
IIOMetadataFormat.VALUE_RANGE_MAX_INCLUSIVE_MASK))
{
case IIOMetadataFormat.VALUE_RANGE: {
final int datatype = format.getAttributeDataType(element, attribute);
valids = ValidValues.range(datatype, type,
format.getAttributeMinValue(element, attribute),
format.getAttributeMaxValue(element, attribute));
break;
}
case IIOMetadataFormat.VALUE_ENUMERATION: {
valids = new ValidValues(format.getAttributeEnumerations(element, attribute));
break;
}
}
}
validValues = valids;
}
return valids.equals(ValidValues.UNRESTRICTED) ? null : valids;
}
/**
* Returns the default value, or {@code null} if none.
*
* @return The default value, or {@code null} if none.
*/
public Object getDefaultValue() {
Object value = defaultValue;
if (value == null) {
final IIOMetadataFormat format = tree.format;
if (attribute == null) {
switch (format.getObjectValueType(element)) {
case IIOMetadataFormat.VALUE_NONE: break;
default: value = format.getObjectDefaultValue(element); break;
}
} else {
value = format.getAttributeDefaultValue(element, attribute);
}
value = convert(value);
if (value == null) {
value = "";
}
defaultValue = value;
}
return value.equals("") ? null : value;
}
/**
* Returns the value of this node, or {@code null} if none. This property is the only one
* which can be modified by a {@linkplain #setUserObject(Object) setter method}. If the
* {@link MetadataTreeTable} contains an {@link IIOMetadata} instance, then the user object
* is initialized to the value extracted from the {@code IIOMetadata}.
*
* @return The user object, or {@code null}.
*/
@Override
public Object getUserObject() {
Object value = super.getUserObject();
if (value == null) {
final Node node = xmlNode;
if (node != null) {
if (attribute != null) {
if (node instanceof Element) {
value = ((Element) node).getAttribute(attribute);
}
} else {
if (node instanceof IIOMetadataNode) {
value = ((IIOMetadataNode) node).getUserObject();
}
if (value == null) {
value = node.getNodeValue();
}
}
}
value = convert(value);
if (value == null) {
value = ""; // Means "no value".
}
super.setUserObject(value);
}
return value.equals("") ? null : value;
}
/**
* Sets the value of this node. The given value must be compliant with the restrictions
* specified by {@link #getValueType()} and {@link #getValueRestriction()}.
*
* @param value The value to give to this node (can be null).
* @throws IllegalArgumentException if the given value is not an instance of the
* {@linkplain #getValueType expected type} or violates a
* {@linkplain #getValueRestriction() value restriction}.
*
* @see #setValueAt(Object, int)
*/
@Override
public void setUserObject(Object value) throws IllegalArgumentException {
final Class<?> type = getValueType();
if (type == null) {
throw new IllegalArgumentException(error(Errors.Keys.IllegalParameterValue_2, value));
}
if (value != null) {
value = convert(value);
if (!type.isInstance(value)) {
throw new IllegalArgumentException(error(Errors.Keys.IllegalParameterType_2, value.getClass()));
}
final ValueRestriction r = getValueRestriction();
if (r != null) {
final Set<?> validValues = r.validValues;
if (validValues != null && !validValues.contains(value)) {
throw new IllegalArgumentException(error(Errors.Keys.IllegalParameterValue_2, value));
}
final NumberRange<?> range = r.range;
// We know we can cast to Comparable since 'value' is an instance of 'type'.
if (range != null && !((NumberRange) range).contains((Comparable) value)) {
throw new IllegalArgumentException(Errors.getResources(tree.getLocale())
.getString(Errors.Keys.ValueOutOfBounds_3,
value, range.getMinDouble(true), range.getMaxDouble(true)));
}
}
} else {
value = ""; // Sentinal value meaning "evaluated to null".
}
super.setUserObject(value);
}
/**
* Sets the user object without argument check.
*/
private void setUserObjectUnsafe(final Object value) {
super.setUserObject(value);
}
/**
* Formats a localized error message. This method is used only for the error messages
* where the first argument is the parameter name.
*/
private String error(final short key, final Object argument) {
return Errors.getResources(tree.getLocale()).getString(key, getLabel(), argument);
}
/**
* Converts the given object to the type expected by this node.
* Returns the object unchanged if no converter is found.
*/
@SuppressWarnings("unchecked")
private Object convert(Object value) {
if (value != null) {
final Class<?> target = getValueType();
if (target != null) {
try {
value = ObjectConverters.convert(value, target);
} catch (UnconvertibleObjectException e) {
Logging.recoverableException(null, MetadataTreeNode.class, "getValue", e);
}
}
}
return value;
}
/**
* Copies the user object from this node to the parent node if the slot is empty.
* This method should be invoked for attribute nodes, when the attribute name is
* not really interesting (e.g. {@code "Value"} or {@code "Name"}).
*
* @return {@code true} if the copy has been performed.
*/
final boolean copyToParent(final MetadataTreeNode parent) {
if (getUserObject() != null) { // Force computation.
if (parent.getUserObject() == null &&
parent.getValueType() == null &&
parent.getDefaultValue() == null &&
parent.getValueRestriction() == null)
{
parent.setUserObjectUnsafe(super.getUserObject());
/*
* The getter methods below are mostly for forcing computation.
* Note that we don't change the occurrence on purpose, since the
* occurrence of attribute is always 1 while the occurrence of the
* parent name is more informatives (e.g. [0..1] is the element
* is optional).
*/
if (getValueType() != null) parent.valueType = valueType;
if (getDefaultValue() != null) parent.defaultValue = defaultValue;
if (getValueRestriction() != null) parent.validValues = validValues;
if (getDescription() != null) {
// This particular case is often defined in the parent node.
if (parent.getDescription() == null) {
parent.description = description;
}
}
return true;
}
}
return false;
}
/**
* Returns the column number to use in {@code switch} statements.
*
* @param column The column visible in public API.
* @return The column number to use in {@code switch} statements.
*/
private int canonical(int column) {
if (!hasValue && column >= VALUE_COLUMN) {
column++; // Skip the "value" column if it doesn't exist.
}
return column;
}
/**
* Returns the number of columns supported by this {@code TreeTableNode}. This method returns
* {@value org.geotoolkit.image.io.metadata.MetadataTreeTable#COLUMN_COUNT} if the tree table
* contains the data of an {@link IIOMetadata} object, or the above value minus one otherwise.
*
* @return The number of columns this node supports.
*/
@Override
public int getColumnCount() {
return hasValue ? COLUMN_COUNT : COLUMN_COUNT-1;
}
/**
* Returns the most specific superclass of values that can be stored in the given column.
* The columns are numbered from 0 inclusive to {@link #getColumnCount()} exclusive. They
* are the same numbers than the ones used for the {@link #getValueAt(int)} method.
*
* @param column The column to query.
* @return The most specific superclass of legal values in the queried column.
*/
@Override
@SuppressWarnings("fallthrough")
public Class<?> getColumnClass(final int column) {
switch (canonical(column)) {
case 0: // The label.
case 1: return String.class; // The description.
case 2: return Class.class; // The base type of values.
case 3: return NumberRange.class; // The range of occurrences
case 6: return ValueRestriction.class; // The restrictions on valid values.
case 5: // The default value.
case VALUE_COLUMN: { // The actual value.
final Class<?> type = getValueType();
if (type != null) {
return type;
}
// fallthrough
}
default: return Object.class;
}
}
/**
* Gets the value for this node that corresponds to a particular tabular column.
* The columns are numbered from 0 inclusive to {@link #getColumnCount()} exclusive.
* Each column maps to a getter methods of this class, in this order:
* <p>
* <ol>
* <li>{@link #getLabel()}</li>
* <li>{@link #getDescription()}</li>
* <li>{@link #getValueType()}</li>
* <li>{@link #getOccurrences()}</li>
* <li>{@link #getUserObject()} (this column may be omitted - see below)</li>
* <li>{@link #getDefaultValue()}</li>
* <li>{@link #getValueRestriction()}</li>
* </ol>
* <p>
* Note that if the tree table does not map a {@link IIOMetadata} object, then there is
* no column for {@code getUserObject()} and the number of all following columns are
* shifted by one.
*
* {@note If the behavior of this method is changed, then <code>IIOMetadataTreeTable</code>
* implementation needs to be modified accordingly.}
*
* @param column The column to query.
* @return The value for the queried column.
*/
@Override
public Object getValueAt(final int column) {
switch (canonical(column)) {
case 0: return getLabel();
case 1: return getDescription();
case 2: return getValueType();
case 3: return getOccurrences();
case VALUE_COLUMN: return getUserObject();
case 5: return getDefaultValue();
case 6: return getValueRestriction();
case COLUMN_COUNT:
// The later is added only for making sure at compile-time that
// we are not declaring more columns than the expected number.
default: return null;
}
}
/**
* Sets the value for the given column. This method {@linkplain #setUserObject(Object) set
* the user object} to the given value only if the all the following conditions are meet:
* <p>
* <ul>
* <li>The given column is the {@link MetadataTreeTable#VALUE_COLUMN VALUE_COLUMN} and
* that column exists (i.e. an instance of {@link IIOMetadata} has been specified
* to {@link MetadataTreeTable}).</li>
* <li>This node accepts values (i.e. the value type is not {@link IIOMetadataFormat#VALUE_NONE}).</li>
* <li>The given value is an instance of the {@linkplain #getValueType() expected type}.</li>
* <li>The given value, if non-null, is compliant with the {@linkplain #getValueRestriction()
* value restrictions}.</li>
* </ul>
* <p>
* Otherwise this method does nothing.
*
* @param value The value to set.
* @param column The column to set the value on.
*/
@Override
public void setValueAt(final Object value, final int column) {
if (hasValue && column == VALUE_COLUMN) {
try {
setUserObject(value);
} catch (IllegalArgumentException e) {
/*
* Ignoring the exception is conform to the specification of this method. This
* is typically a consequence of the user having edited a cell in a JTable with
* an invalid value. The JTable behavior is to discart the user edition and restore
* the previous value. If we want a more sophesticated behavior with a warning that
* the user provided an invalid value, then we need a custom TableCellEditor.
*/
Logging.recoverableException(null, MetadataTreeNode.class, "setValueAt", e);
}
}
}
/**
* Determines whether the specified column is editable. By default only the
* {@link MetadataTreeTable#VALUE_COLUMN} is editable, and only if that column exists.
* This column does not exist if no {@link IIOMetadata} instance was specified
* to {@link MetadataTreeTable}.
*
* @param column The column to query.
* @return {@code true} if the column is editable, false otherwise.
*/
@Override
public boolean isEditable(final int column) {
return hasValue && column == VALUE_COLUMN;
}
}