001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2007, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.metadata;
017:
018: // J2SE dependencies
019: import java.lang.reflect.Constructor;
020: import java.text.DateFormat;
021: import java.text.NumberFormat;
022: import java.util.Collection;
023: import java.util.Date;
024: import java.util.Map;
025: import java.util.Iterator;
026: import java.util.Locale;
027: import javax.swing.tree.DefaultMutableTreeNode;
028: import javax.swing.tree.MutableTreeNode;
029: import javax.swing.tree.TreeNode;
030:
031: // OpenGIS dependencies
032: import org.opengis.util.CodeList;
033: import org.opengis.util.InternationalString;
034:
035: // Geotools dependencies
036: import org.geotools.resources.XMath;
037: import org.geotools.resources.Utilities;
038: import org.geotools.resources.OptionalDependencies;
039:
040: /**
041: * Represents the metadata property as a tree made from {@linkplain TreeNode tree nodes}.
042: * Note that while {@link TreeNode} is defined in the {@link javax.swing.tree} package,
043: * it can be seen as a data structure independent of Swing.
044: * <p>
045: * Note: this method is called {@code PropertyTree} because it may implements
046: * {@link javax.swing.tree.TreeModel} in some future Geotools implementation.
047: *
048: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/metadata/PropertyTree.java $
049: * @version $Id: PropertyTree.java 25476 2007-05-09 17:24:32Z desruisseaux $
050: * @author Martin Desruisseaux (Geomatys)
051: */
052: final class PropertyTree {
053: /**
054: * The default number of significant digits (may or may not be fraction digits).
055: */
056: private static final int PRECISION = 12;
057:
058: /**
059: * The expected standard implemented by the metadata.
060: */
061: private final MetadataStandard standard;
062:
063: /**
064: * The locale to use for {@linkplain Date date}, {@linkplain Number number}
065: * and {@linkplain InternationalString international string} formatting.
066: */
067: private final Locale locale;
068:
069: /**
070: * The object to use for formatting numbers.
071: * Will be created only when first needed.
072: */
073: private transient NumberFormat numberFormat;
074:
075: /**
076: * The object to use for formatting dates.
077: * Will be created only when first needed.
078: */
079: private transient DateFormat dateFormat;
080:
081: /**
082: * Creates a new tree builder using the default locale.
083: *
084: * @param standard The expected standard implemented by the metadata.
085: */
086: public PropertyTree(final MetadataStandard standard) {
087: this (standard, Locale.getDefault());
088: }
089:
090: /**
091: * Creates a new tree builder.
092: *
093: * @param standard The expected standard implemented by the metadata.
094: * @param locale The locale to use for {@linkplain Date date}, {@linkplain Number number}
095: * and {@linkplain InternationalString international string} formatting.
096: */
097: public PropertyTree(final MetadataStandard standard,
098: final Locale locale) {
099: this .standard = standard;
100: this .locale = locale;
101: }
102:
103: /**
104: * Creates a tree for the specified metadata.
105: */
106: public MutableTreeNode asTree(final Object metadata) {
107: final String name = Utilities.getShortName(standard
108: .getInterface(metadata.getClass()));
109: final DefaultMutableTreeNode root = OptionalDependencies
110: .createTreeNode(localize(name), metadata, true);
111: append(root, metadata);
112: return root;
113: }
114:
115: /**
116: * Appends the specified value to a branch. The value may be a metadata
117: * (treated {@linkplain AbstractMetadata#asMap as a Map} - see below),
118: * a collection or a singleton.
119: * <p>
120: * Map or metadata are constructed as a sub tree where every nodes is a
121: * property name, and the childs are the value(s) for that property.
122: */
123: private void append(final DefaultMutableTreeNode branch,
124: final Object value) {
125: if (value instanceof Map) {
126: appendMap(branch, (Map) value);
127: return;
128: }
129: if (value instanceof AbstractMetadata) {
130: appendMap(branch, ((AbstractMetadata) value).asMap());
131: return;
132: }
133: if (value != null) {
134: final PropertyAccessor accessor = standard
135: .getAccessorOptional(value.getClass());
136: if (accessor != null) {
137: appendMap(branch, new PropertyMap(value, accessor));
138: return;
139: }
140: }
141: if (value instanceof Collection) {
142: for (final Iterator it = ((Collection) value).iterator(); it
143: .hasNext();) {
144: final Object element = it.next();
145: if (!PropertyAccessor.isEmpty(element)) {
146: append(branch, element);
147: }
148: }
149: return;
150: }
151: final String asText;
152: if (value instanceof CodeList) {
153: asText = localize((CodeList) value);
154: } else if (value instanceof Date) {
155: asText = format((Date) value);
156: } else if (value instanceof Number) {
157: asText = format((Number) value);
158: } else if (value instanceof InternationalString) {
159: asText = ((InternationalString) value).toString(locale);
160: } else {
161: asText = String.valueOf(value);
162: }
163: branch.add(OptionalDependencies.createTreeNode(asText, value,
164: false));
165: }
166:
167: /**
168: * Appends the specified map (usually a metadata) to a branch. Each map keys
169: * is a child in the specified {@code branch}, and each value is a child of
170: * the map key. There is often only one value for a map key, but not always;
171: * some are collections, which are formatted as many childs for the same key.
172: */
173: private void appendMap(final DefaultMutableTreeNode branch,
174: final Map asMap) {
175: for (final Iterator it = asMap.entrySet().iterator(); it
176: .hasNext();) {
177: final Map.Entry entry = (Map.Entry) it.next();
178: final Object value = entry.getValue();
179: if (!PropertyAccessor.isEmpty(value)) {
180: final String name = localize((String) entry.getKey());
181: final DefaultMutableTreeNode child = OptionalDependencies
182: .createTreeNode(name, value, true);
183: append(child, value);
184: branch.add(child);
185: }
186: }
187: }
188:
189: /**
190: * Formats the specified number.
191: */
192: private String format(final Number value) {
193: if (numberFormat == null) {
194: numberFormat = NumberFormat.getNumberInstance(locale);
195: numberFormat.setMinimumFractionDigits(0);
196: }
197: int precision = 0;
198: if (!XMath.isInteger(value.getClass())) {
199: precision = PRECISION;
200: final double v = Math.abs(value.doubleValue());
201: if (v > 0) {
202: final int digits = (int) XMath.log10(v);
203: if (Math.abs(digits) >= PRECISION) {
204: // TODO: Switch to exponential notation when a convenient API will be available in J2SE.
205: return value.toString();
206: }
207: if (digits >= 0) {
208: precision -= digits;
209: }
210: precision = Math.max(0, PRECISION - precision);
211: }
212: }
213: numberFormat.setMaximumFractionDigits(precision);
214: return numberFormat.format(value);
215: }
216:
217: /**
218: * Formats the specified date.
219: */
220: private String format(final Date value) {
221: if (dateFormat == null) {
222: dateFormat = DateFormat.getDateTimeInstance(
223: DateFormat.LONG, DateFormat.LONG, locale);
224: }
225: return dateFormat.format(value);
226: }
227:
228: /**
229: * Localize the specified property name. In current version, this is merely
230: * a hook for future development. For now we reformat the programatic name.
231: */
232: private String localize(String name) {
233: name = name.trim();
234: final int length = name.length();
235: if (length != 0) {
236: final StringBuffer buffer = new StringBuffer();
237: buffer.append(Character.toUpperCase(name.charAt(0)));
238: boolean previousIsUpper = true;
239: int base = 1;
240: for (int i = 1; i < length; i++) {
241: final boolean currentIsUpper = Character
242: .isUpperCase(name.charAt(i));
243: if (currentIsUpper != previousIsUpper) {
244: /*
245: * When a case change is detected (lower case to upper case as in "someName",
246: * or "someURL", or upper case to lower case as in "HTTPProxy"), then insert
247: * a space just before the upper case letter.
248: */
249: int split = i;
250: if (previousIsUpper) {
251: split--;
252: }
253: if (split > base) {
254: buffer.append(name.substring(base, split))
255: .append(' ');
256: base = split;
257: }
258: }
259: previousIsUpper = currentIsUpper;
260: }
261: final String candidate = buffer
262: .append(name.substring(base)).toString();
263: if (!candidate.equals(name)) {
264: // Holds a reference to this new String object only if it worth it.
265: name = candidate;
266: }
267: }
268: return name;
269: }
270:
271: /**
272: * Localize the specified property name. In current version, this is merely
273: * a hook for future development. For now we reformat the programatic name.
274: */
275: private String localize(final CodeList code) {
276: return code.name().trim().replace('_', ' ').toLowerCase(locale);
277: }
278:
279: /**
280: * Returns a string representation of the specified tree node.
281: */
282: public static String toString(final TreeNode node) {
283: final StringBuffer buffer = new StringBuffer();
284: toString(node, buffer, 0, System.getProperty("line.separator",
285: "\n"));
286: return buffer.toString();
287: }
288:
289: /**
290: * Append a string representation of the specified node to the specified buffer.
291: */
292: private static void toString(final TreeNode node,
293: final StringBuffer buffer, final int indent,
294: final String lineSeparator) {
295: final int count = node.getChildCount();
296: if (count == 0) {
297: if (node.isLeaf()) {
298: /*
299: * If the node has no child and is a leaf, then it is some value like a number,
300: * a date or a string. We just display this value, which is usually part of a
301: * collection. If the node has no child and is NOT a leaf, then it is an empty
302: * metadata and we just ommit it.
303: */
304: buffer.append(Utilities.spaces(indent)).append(node)
305: .append(lineSeparator);
306: }
307: return;
308: }
309: buffer.append(Utilities.spaces(indent)).append(node)
310: .append(':');
311: if (count == 1) {
312: final TreeNode child = node.getChildAt(0);
313: if (child.isLeaf()) {
314: buffer.append(' ').append(child).append(lineSeparator);
315: return;
316: }
317: }
318: for (int i = 0; i < count; i++) {
319: final TreeNode child = node.getChildAt(i);
320: if (i == 0) {
321: buffer.append(lineSeparator);
322: }
323: toString(child, buffer, indent + 2, lineSeparator);
324: }
325: }
326: }
|