/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2007-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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.metadata;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Date;
import java.util.Map;
import java.util.Iterator;
import java.util.Locale;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import org.opengis.util.CodeList;
import org.opengis.util.InternationalString;
import org.geotools.util.Utilities;
import org.geotools.resources.Classes;
import org.geotools.resources.OptionalDependencies;
/**
* Represents the metadata property as a tree made from {@linkplain TreeNode tree nodes}.
* Note that while {@link TreeNode} is defined in the {@link javax.swing.tree} package,
* it can be seen as a data structure independent of Swing.
* <p>
* Note: this method is called {@code PropertyTree} because it may implements
* {@link javax.swing.tree.TreeModel} in some future Geotools implementation.
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (Geomatys)
*/
final class PropertyTree {
/**
* The default number of significant digits (may or may not be fraction digits).
*/
private static final int PRECISION = 12;
/**
* The expected standard implemented by the metadata.
*/
private final MetadataStandard standard;
/**
* The locale to use for {@linkplain Date date}, {@linkplain Number number}
* and {@linkplain InternationalString international string} formatting.
*/
private final Locale locale;
/**
* The object to use for formatting numbers.
* Will be created only when first needed.
*/
private transient NumberFormat numberFormat;
/**
* The object to use for formatting dates.
* Will be created only when first needed.
*/
private transient DateFormat dateFormat;
/**
* Creates a new tree builder using the default locale.
*
* @param standard The expected standard implemented by the metadata.
*/
public PropertyTree(final MetadataStandard standard) {
this(standard, Locale.getDefault());
}
/**
* Creates a new tree builder.
*
* @param standard The expected standard implemented by the metadata.
* @param locale The locale to use for {@linkplain Date date}, {@linkplain Number number}
* and {@linkplain InternationalString international string} formatting.
*/
public PropertyTree(final MetadataStandard standard, final Locale locale) {
this.standard = standard;
this.locale = locale;
}
/**
* Creates a tree for the specified metadata.
*/
public MutableTreeNode asTree(final Object metadata) {
final String name = Classes.getShortName(standard.getInterface(metadata.getClass()));
final DefaultMutableTreeNode root =
OptionalDependencies.createTreeNode(localize(name), metadata, true);
append(root, metadata);
return root;
}
/**
* Appends the specified value to a branch. The value may be a metadata
* (treated {@linkplain AbstractMetadata#asMap as a Map} - see below),
* a collection or a singleton.
* <p>
* Map or metadata are constructed as a sub tree where every nodes is a
* property name, and the childs are the value(s) for that property.
*/
private void append(final DefaultMutableTreeNode branch, final Object value) {
if (value instanceof Map) {
appendMap(branch, (Map) value);
return;
}
if (value instanceof AbstractMetadata) {
appendMap(branch, ((AbstractMetadata) value).asMap());
return;
}
if (value != null) {
final PropertyAccessor accessor = standard.getAccessorOptional(value.getClass());
if (accessor != null) {
appendMap(branch, new PropertyMap(value, accessor));
return;
}
}
if (value instanceof Collection) {
for (final Iterator it=((Collection) value).iterator(); it.hasNext();) {
final Object element = it.next();
if (!PropertyAccessor.isEmpty(element)) {
append(branch, element);
}
}
return;
}
final String asText;
if (value instanceof CodeList) {
asText = localize((CodeList) value);
} else if (value instanceof Date) {
asText = format((Date) value);
} else if (value instanceof Number) {
asText = format((Number) value);
} else if (value instanceof InternationalString) {
asText = ((InternationalString) value).toString(locale);
} else {
asText = String.valueOf(value);
}
branch.add(OptionalDependencies.createTreeNode(asText, value, false));
}
/**
* Appends the specified map (usually a metadata) to a branch. Each map keys
* is a child in the specified {@code branch}, and each value is a child of
* the map key. There is often only one value for a map key, but not always;
* some are collections, which are formatted as many childs for the same key.
*/
private void appendMap(final DefaultMutableTreeNode branch, final Map asMap) {
for (final Iterator it=asMap.entrySet().iterator(); it.hasNext();) {
final Map.Entry entry = (Map.Entry) it.next();
final Object value = entry.getValue();
if (!PropertyAccessor.isEmpty(value)) {
final String name = localize((String) entry.getKey());
final DefaultMutableTreeNode child =
OptionalDependencies.createTreeNode(name, value, true);
append(child, value);
branch.add(child);
}
}
}
/**
* Formats the specified number.
*/
private String format(final Number value) {
if (numberFormat == null) {
numberFormat = NumberFormat.getNumberInstance(locale);
numberFormat.setMinimumFractionDigits(0);
}
int precision = 0;
if (!Classes.isInteger(value.getClass())) {
precision = PRECISION;
final double v = Math.abs(value.doubleValue());
if (v > 0) {
final int digits = (int) Math.log10(v);
if (Math.abs(digits) >= PRECISION) {
// TODO: Switch to exponential notation when a convenient API will be available in J2SE.
return value.toString();
}
if (digits >= 0) {
precision -= digits;
}
precision = Math.max(0, PRECISION - precision);
}
}
numberFormat.setMaximumFractionDigits(precision);
return numberFormat.format(value);
}
/**
* Formats the specified date.
*/
private String format(final Date value) {
if (dateFormat == null) {
dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
}
return dateFormat.format(value);
}
/**
* Localize the specified property name. In current version, this is merely
* a hook for future development. For now we reformat the programatic name.
*/
private String localize(String name) {
name = name.trim();
final int length = name.length();
if (length != 0) {
final StringBuilder buffer = new StringBuilder();
buffer.append(Character.toUpperCase(name.charAt(0)));
boolean previousIsUpper = true;
int base = 1;
for (int i=1; i<length; i++) {
final boolean currentIsUpper = Character.isUpperCase(name.charAt(i));
if (currentIsUpper != previousIsUpper) {
/*
* When a case change is detected (lower case to upper case as in "someName",
* or "someURL", or upper case to lower case as in "HTTPProxy"), then insert
* a space just before the upper case letter.
*/
int split = i;
if (previousIsUpper) {
split--;
}
if (split > base) {
buffer.append(name.substring(base, split)).append(' ');
base = split;
}
}
previousIsUpper = currentIsUpper;
}
final String candidate = buffer.append(name.substring(base)).toString();
if (!candidate.equals(name)) {
// Holds a reference to this new String object only if it worth it.
name = candidate;
}
}
return name;
}
/**
* Localize the specified property name. In current version, this is merely
* a hook for future development. For now we reformat the programatic name.
*/
private String localize(final CodeList code) {
return code.name().trim().replace('_', ' ').toLowerCase(locale);
}
/**
* Returns a string representation of the specified tree node.
*/
public static String toString(final TreeNode node) {
final StringBuilder buffer = new StringBuilder();
toString(node, buffer, 0, System.getProperty("line.separator", "\n"));
return buffer.toString();
}
/**
* Append a string representation of the specified node to the specified buffer.
*/
private static void toString(final TreeNode node,
final StringBuilder buffer,
final int indent,
final String lineSeparator)
{
final int count = node.getChildCount();
if (count == 0) {
if (node.isLeaf()) {
/*
* If the node has no child and is a leaf, then it is some value like a number,
* a date or a string. We just display this value, which is usually part of a
* collection. If the node has no child and is NOT a leaf, then it is an empty
* metadata and we just ommit it.
*/
buffer.append(Utilities.spaces(indent)).append(node).append(lineSeparator);
}
return;
}
buffer.append(Utilities.spaces(indent)).append(node).append(':');
if (count == 1) {
final TreeNode child = node.getChildAt(0);
if (child.isLeaf()) {
buffer.append(' ').append(child).append(lineSeparator);
return;
}
}
for (int i=0; i<count; i++) {
final TreeNode child = node.getChildAt(i);
if (i == 0) {
buffer.append(lineSeparator);
}
toString(child, buffer, indent+2, lineSeparator);
}
}
}