001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2007, GeoTools Project Managment Committee (PMC)
005: * (C) 2007, Geomatys
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation;
010: * version 2.1 of the License.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.image.io.metadata;
018:
019: // J2SE dependencies
020: import java.lang.reflect.Array;
021: import java.util.ArrayList;
022: import java.util.Collection;
023: import java.util.Collections;
024: import java.util.Date;
025: import java.util.Iterator;
026: import java.util.LinkedHashSet;
027: import java.util.List;
028: import java.util.NoSuchElementException;
029: import java.util.StringTokenizer;
030: import java.util.logging.Level;
031: import java.util.logging.LogRecord;
032: import javax.imageio.metadata.IIOMetadataNode;
033: import org.w3c.dom.Element;
034: import org.w3c.dom.Node;
035: import org.w3c.dom.NodeList;
036:
037: // Geotools dependencies
038: import org.geotools.resources.XMath;
039: import org.geotools.resources.Utilities;
040: import org.geotools.resources.i18n.Errors;
041: import org.geotools.resources.i18n.ErrorKeys;
042: import org.geotools.resources.OptionalDependencies;
043: import org.geotools.util.UnsupportedImplementationException;
044:
045: /**
046: * Base class for {@linkplain GeographicMetadata geographic metadata} parsers. This class
047: * provides convenience methods for encoding and decoding metadata information. A metadata
048: * root {@linkplain Node node} is specified at construction time, together with a path to
049: * the {@linkplain Element element} of interest. Example of valid paths:
050: * <p>
051: * <ul>
052: * <li>{@code "CoordinateReferenceSystem/Datum"}</li>
053: * <li>{@code "CoordinateReferenceSystem/CoordinateSystem"}</li>
054: * <li>{@code "GridGeometry/Envelope"}</li>
055: * </ul>
056: * <p>
057: * In addition, some elements contains an arbitrary amount of childs. The path to child
058: * elements can also be specified to the constructor. Examples (note that the constructor
059: * expects paths relative to the parent; we show absolute paths below for completness):
060: * <p>
061: * <ul>
062: * <li>{@code "CoordinateReferenceSystem/CoordinateSystem/Axis"}</li>
063: * <li>{@code "GridGeometry/Envelope/CoordinateValues"}</li>
064: * <li>{@code "SampleDimensions/SampleDimension"}</li>
065: * </ul>
066: *
067: * The {@code get} and {@code set} methods defined in this class will operate on the
068: * <cite>selected</cite> {@linkplain Element element}, which may be either the one
069: * specified at construction time, or one of its childs. The element can be selected
070: * by {@link #selectParent} (the default) or {@link #selectChild}.
071: * <p>
072: * The example below creates an accessor for a node called {@code "CoordinateSystem"}
073: * which is expected to have childs called {@code "Axis"}:
074: *
075: * <blockquote><pre>
076: * MetadataAccessor accessor = new MetadataAccessor(metadata,
077: * "CoordinateReferenceSystem/CoordinateSystem", "Axis");
078: *
079: * accessor.selectParent();
080: * String csName = accessor.getString("name");
081: *
082: * accessor.selectChild(0);
083: * String firstAxisName = accessor.getString("name");
084: * </pre></blockquote>
085: *
086: * @since 2.4
087: * @version $Id: MetadataAccessor.java 27583 2007-10-23 11:29:26Z desruisseaux $
088: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/unsupported/coverageio/src/main/java/org/geotools/image/io/metadata/MetadataAccessor.java $
089: * @author Martin Desruisseaux
090: */
091: public class MetadataAccessor {
092: /**
093: * The separator between names in a node path.
094: */
095: private static final char SEPARATOR = '/';
096:
097: /**
098: * The owner of this accessor.
099: */
100: private final GeographicMetadata metadata;
101:
102: /**
103: * The parent of child {@linkplain Element elements}.
104: */
105: private final Node parent;
106:
107: /**
108: * The {@linkplain #childs} path. This is the {@code childPath} parameter
109: * given to the constructor.
110: */
111: private final String childPath;
112:
113: /**
114: * The list of child elements. May be empty but never null.
115: */
116: private final List/*<Node>*/childs;
117:
118: /**
119: * The current element, or {@code null} if not yet selected.
120: *
121: * @see #selectChild
122: * @see #currentElement()
123: */
124: private transient Element current;
125:
126: /**
127: * {@code true} if warnings are enabled.
128: */
129: private transient boolean warningsEnabled = true;
130:
131: /**
132: * Creates an accessor with the same parent and childs than the specified one. The two
133: * accessors will share the same {@linkplain Node metadata nodes} (including the list
134: * of childs), so change in one accessor will be immediately reflected in the other
135: * accessor. However each accessor can {@linkplain #selectChild select their child}
136: * independently.
137: * <p>
138: * The main purpose of this constructor is to create many views over the same list
139: * of childs, where each view {@linkplain #selectChild select} a different child.
140: */
141: protected MetadataAccessor(final MetadataAccessor clone) {
142: metadata = clone.metadata;
143: parent = clone.parent;
144: childPath = clone.childPath;
145: childs = clone.childs;
146: }
147:
148: /**
149: * Creates an accessor for the {@linkplain Element element} at the given path. Paths are
150: * separated by the {@code '/'} character. See {@linkplain MetadataAccessor class javadoc}
151: * for path examples.
152: *
153: * @param metadata The metadata node.
154: * @param parentPath The path to the {@linkplain Node node} of interest, or {@code null}
155: * if the {@code metadata} root node is directly the node of interest.
156: * @param childPath The path (relative to {@code parentPath}) to the child
157: * {@linkplain Element elements}, or {@code null} if none.
158: */
159: protected MetadataAccessor(final GeographicMetadata metadata,
160: final String parentPath, final String childPath) {
161: this .metadata = metadata;
162: final Node root = metadata.getRootNode();
163: /*
164: * Fetchs the parent node and ensure that we got a singleton. If there is more nodes than
165: * expected, log a warning and pickup the first one. If there is no node, create a new one.
166: */
167: final List childs = new ArrayList(4);
168: if (parentPath != null) {
169: listChilds(root, parentPath, 0, childs, true);
170: final int count = childs.size();
171: switch (count) {
172: default: {
173: warning("<init>", ErrorKeys.TOO_MANY_OCCURENCES_$2,
174: new Object[] { parentPath, new Integer(count) });
175: // Fall through for picking the first node.
176: }
177: case 1: {
178: parent = (Node) childs.get(0);
179: childs.clear();
180: break;
181: }
182: case 0: {
183: parent = appendChild(root, parentPath);
184: break;
185: }
186: }
187: } else {
188: parent = root;
189: }
190: /*
191: * Computes a full path to children. Searching from 'metadata' root node using 'path'
192: * should be identical to searching from 'parent' node using 'childPath', except in
193: * case of badly formed metadata where the parent node appears more than once.
194: */
195: this .childPath = childPath;
196: if (childPath != null) {
197: final String path;
198: if (parentPath != null) {
199: path = parentPath + SEPARATOR + childPath;
200: } else {
201: path = childPath;
202: }
203: listChilds(root, path, 0, childs, false);
204: this .childs = childs;
205: } else {
206: this .childs = Collections.EMPTY_LIST;
207: }
208: if (parent instanceof Element) {
209: current = (Element) parent;
210: }
211: }
212:
213: /**
214: * Adds to the {@link #childs} list the child nodes at the given {@code path}.
215: * This method is for constructor implementation only and invokes itself recursively.
216: *
217: * @param parent The parent metadata node.
218: * @param path The path to the nodes or elements to insert into the list.
219: * @param base The offset in {@code path} for the next element name.
220: * @param childs The list where to insert the nodes or elements.
221: * @param includeNodes {@code true} of adding nodes as well as elements.
222: */
223: private static void listChilds(final Node parent,
224: final String path, final int base,
225: final List/*<Node>*/childs, final boolean includeNodes) {
226: final int upper = path.indexOf(SEPARATOR, base);
227: final String name = ((upper >= 0) ? path.substring(base, upper)
228: : path.substring(base)).trim();
229: final NodeList list = parent.getChildNodes();
230: final int length = list.getLength();
231: for (int i = 0; i < length; i++) {
232: final Node candidate = list.item(i);
233: if (name.equals(candidate.getNodeName())) {
234: if (upper >= 0) {
235: listChilds(candidate, path, upper + 1, childs,
236: includeNodes);
237: } else if (includeNodes
238: || (candidate instanceof Element)) {
239: // For the very last node, we may require an element.
240: childs.add(candidate);
241: }
242: }
243: }
244: }
245:
246: /**
247: * Appends a child to the given parent.
248: *
249: * @param parent The parent to add a child to.
250: * @param path The path of the child to add.
251: * @return element The new child.
252: */
253: private static Node appendChild(Node parent, final String path) {
254: int lower = 0;
255: search: for (int upper; (upper = path.indexOf(SEPARATOR, lower)) >= 0; lower = upper + 1) {
256: final String name = path.substring(lower, upper).trim();
257: final NodeList list = parent.getChildNodes();
258: final int length = list.getLength();
259: for (int i = length; --i >= 0;) {
260: final Node candidate = list.item(i);
261: if (name.equals(candidate.getNodeName())) {
262: parent = candidate;
263: continue search;
264: }
265: }
266: parent = parent.appendChild(new IIOMetadataNode(name
267: .intern()));
268: }
269: final String name = path.substring(lower).trim().intern();
270: return parent.appendChild(new IIOMetadataNode(name));
271: }
272:
273: /**
274: * Returns the number of child {@linkplain Element elements}.
275: * This is the upper value (exclusive) for {@link #selectChild}.
276: *
277: * @return The child {@linkplain Element elements} count.
278: *
279: * @see #selectChild
280: * @see #appendChild
281: */
282: protected int childCount() {
283: return childs.size();
284: }
285:
286: /**
287: * Adds a new child {@linkplain Element element} at the path given at construction time.
288: * The {@linkplain #childCount child count} will be increased by 1.
289: * <p>
290: * The new child is <strong>not</strong> automatically selected. In order to select this
291: * new child, the {@link #selectChild} method must be invoked explicitly.
292: *
293: * @return The index of the new child element.
294: *
295: * @see #childCount
296: * @see #selectChild
297: */
298: protected int appendChild() {
299: final int size = childs.size();
300: final Node child = appendChild(parent, childPath);
301: if (child instanceof Element) {
302: childs.add((Element) child);
303: return size;
304: } else {
305: throw new UnsupportedImplementationException(child
306: .getClass());
307: }
308: }
309:
310: /**
311: * Selects the {@linkplain Element element} at the given index. Every subsequent calls
312: * to {@code get} or {@code set} methods will apply to this selected child element.
313: *
314: * @param index The index of the element to select.
315: * @throws IndexOutOfBoundsException if the specified index is out of bounds.
316: *
317: * @see #childCount
318: * @see #appendChild
319: * @see #selectParent
320: */
321: protected void selectChild(final int index)
322: throws IndexOutOfBoundsException {
323: current = (Element) childs.get(index);
324: }
325:
326: /**
327: * Selects the <em>parent</em> of child elements. Every subsequent calls to {@code get}
328: * or {@code set} methods will apply to this parent element.
329: *
330: * @throws NoSuchElementException if there is no parent {@linkplain Element element}.
331: *
332: * @see #selectChild
333: */
334: protected void selectParent() throws NoSuchElementException {
335: if (parent instanceof Element) {
336: current = (Element) parent;
337: } else {
338: throw new NoSuchElementException();
339: }
340: }
341:
342: /**
343: * Returns the current element.
344: *
345: * @return The currently selected element.
346: * @throws IllegalStateException if there is no selected element.
347: *
348: * @see #selectChild
349: */
350: private Element currentElement() throws IllegalStateException {
351: if (current == null) {
352: throw new IllegalStateException();
353: }
354: return current;
355: }
356:
357: /**
358: * Returns the {@linkplain IIOMetadataNode#getUserObject user object} associated with the
359: * {@linkplain #selectChild selected element}, or {@code null} if none. If no user object
360: * is defined for the element, then the {@linkplain Node#getNodeValue node value} is returned
361: * as a fallback. This is consistent with {@link #setUserObject} implementation, and allows
362: * some parsing of nodes that are not {@link IIOMetadataNode} instances.
363: * <p>
364: * The {@code getUserObject} methods are the only ones to not parse the value returned by
365: * {@link #getString}.
366: *
367: * @return The user object, or {@code null} if none.
368: *
369: * @see #getUserObject(Class)
370: * @see #setUserObject
371: */
372: protected Object getUserObject() {
373: final Element element = currentElement();
374: if (element instanceof IIOMetadataNode) {
375: final Object candidate = ((IIOMetadataNode) element)
376: .getUserObject();
377: if (candidate != null) {
378: return candidate;
379: }
380: }
381: /*
382: * getNodeValue() returns a String. We use it as a fallback, but in typical
383: * IIOMetadataNode usage this value is not used (according its javadoc), so
384: * it will often be null.
385: */
386: return element.getNodeValue();
387: }
388:
389: /**
390: * Returns the user object associated as an instance of the specified class. If the value
391: * returned by {@link #getUserObject()} is not of the expected type, then this method will
392: * tries to parse it as a string.
393: *
394: * @param type The expected class.
395: * @return The user object, or {@code null} if none.
396: * @throws ClassCastException if the user object can not be casted to the specified type.
397: *
398: * @see #getUserObject()
399: * @see #setUserObject
400: */
401: protected Object /*T*/getUserObject(Class/*<T>*/type)
402: throws ClassCastException {
403: type = XMath.primitiveToWrapper(type);
404: Object value = getUserObject();
405: if (value instanceof CharSequence) {
406: if (Number.class.isAssignableFrom(type)) {
407: value = XMath.valueOf(type, value.toString());
408: } else {
409: final Class component = XMath.primitiveToWrapper(type
410: .getComponentType());
411: if (Double.class.equals(component)) {
412: value = parseSequence(value.toString(), false,
413: false);
414: } else if (Integer.class.equals(component)) {
415: value = parseSequence(value.toString(), false, true);
416: }
417: }
418: }
419: return value; // TODO: use type.cast with Java 5.
420: }
421:
422: /**
423: * Sets the {@linkplain IIOMetadataNode#setUserObject user object} associated with the
424: * {@linkplain #selectChild selected element}. This is the only {@code set} method that
425: * doesn't invoke {@link #setString} with a formatted value.
426: * <p>
427: * If the specified value is formattable (i.e. is a {@linkplain CharSequence character
428: * sequence}, a {@linkplain Number number} or an array of the above), then this method
429: * also {@linkplain IIOMetadataNode#setNodeValue sets the node value} as a string. This
430: * is mostly a convenience for formatting purpose since {@link IIOMetadataNode} don't
431: * use the node value. But it may help some libraries that are not designed to work with
432: * with user objects, since they are particular to Image I/O metadata.
433: *
434: * @param value The user object, or {@code null} if none.
435: * @throws UnsupportedImplementationException if the selected element is not an instance of
436: * {@link IIOMetadataNode}.
437: *
438: * @see #getUserObject()
439: */
440: protected void setUserObject(final Object value)
441: throws UnsupportedImplementationException {
442: final Element element = currentElement();
443: final String asText;
444: if (isFormattable(value)) {
445: asText = value.toString();
446: } else if (value != null
447: && isFormattable(value.getClass().getComponentType())) {
448: asText = formatSequence(value);
449: } else {
450: asText = null;
451: }
452: if (element instanceof IIOMetadataNode) {
453: ((IIOMetadataNode) element).setUserObject(value);
454: } else if (value != null && asText == null) {
455: throw new UnsupportedImplementationException(Errors.format(
456: ErrorKeys.ILLEGAL_CLASS_$2, Utilities
457: .getShortClassName(element), Utilities
458: .getShortName(IIOMetadataNode.class)));
459: }
460: element.setNodeValue(asText);
461: }
462:
463: /**
464: * Returns {@code true} if the specified value can be formatted as a text.
465: * We allows formatting only for reasonably cheap objects, for example a
466: * Number but not a CoordinateReferenceSystem.
467: */
468: private static boolean isFormattable(final Object value) {
469: return (value instanceof CharSequence)
470: || (value instanceof Number);
471: }
472:
473: /**
474: * Returns an attribute as a string for the {@linkplain #selectChild selected element},
475: * or {@code null} if none. This method never returns an empty string.
476: * <p>
477: * Every {@code get} methods in this class except {@link #getUserObject getUserObject}
478: * invoke this method first. Consequently, this method provides a single point for
479: * overriding if subclasses want to process the attribute before parsing.
480: *
481: * @param attribute The attribute to fetch (e.g. {@code "name"}).
482: * @return The attribute value (never an empty string), or {@code null} if none.
483: */
484: protected String getString(final String attribute) {
485: String candidate = currentElement().getAttribute(attribute);
486: if (candidate != null) {
487: candidate = candidate.trim();
488: if (candidate.length() == 0) {
489: candidate = null;
490: }
491: }
492: return candidate;
493: }
494:
495: /**
496: * Set the attribute to the specified value,
497: * or remove the attribute if the value is null.
498: * <p>
499: * Every {@code set} methods in this class except {@link #setUserObject setUserObject}
500: * invoke this method last. Consequently, this method provides a single point for
501: * overriding if subclasses want to process the attribute after formatting.
502: *
503: * @param attribute The attribute name.
504: * @param value The attribute value.
505: */
506: protected void setString(final String attribute, String value) {
507: final Element element = currentElement();
508: if (value == null || (value = value.trim()).length() == 0) {
509: if (element.hasAttribute(attribute)) {
510: element.removeAttribute(attribute);
511: }
512: } else {
513: element.setAttribute(attribute, value);
514: }
515: }
516:
517: /**
518: * Set the attribute to the specified enumeration value,
519: * or remove the attribute if the value is null.
520: *
521: * @param attribute The attribute name.
522: * @param value The attribute value.
523: * @param enums The set of allowed values, or {@code null} if unknown.
524: */
525: final void setEnum(final String attribute, String value,
526: final Collection enums) {
527: if (value != null) {
528: value = value.replace('_', ' ').trim();
529: for (final Iterator it = enums.iterator(); it.hasNext();) {
530: final String e = (String) it.next();
531: if (value.equalsIgnoreCase(e)) {
532: value = e;
533: break;
534: }
535: }
536: }
537: setString(attribute, value);
538: }
539:
540: /**
541: * Returns an attribute as an integer for the {@linkplain #selectChild selected element},
542: * or {@code null} if none. If the attribute can't be parsed as an integer, then this method
543: * logs a warning and returns {@code null}.
544: *
545: * @param attribute The attribute to fetch (e.g. {@code "minimum"}).
546: * @return The attribute value, or {@code null} if none or unparseable.
547: */
548: protected Integer getInteger(final String attribute) {
549: String value = getString(attribute);
550: if (value != null) {
551: value = trimFractionalPart(value);
552: try {
553: return Integer.valueOf(value);
554: } catch (NumberFormatException e) {
555: warning("getInteger", ErrorKeys.UNPARSABLE_NUMBER_$1,
556: value);
557: }
558: }
559: return null;
560: }
561:
562: /**
563: * Set the attribute to the specified integer value.
564: *
565: * @param attribute The attribute name.
566: * @param value The attribute value.
567: */
568: protected void setInteger(final String attribute, final int value) {
569: setString(attribute, Integer.toString(value));
570: }
571:
572: /**
573: * Returns an attribute as an array of integers for the {@linkplain #selectChild selected
574: * element}, or {@code null} if none. If an element can't be parsed as an integer, then this
575: * method logs a warning and returns {@code null}.
576: *
577: * @param attribute The attribute to fetch (e.g. {@code "minimum"}).
578: * @param unique {@code true} if duplicated values should be collapsed into unique values,
579: * or {@code false} for preserving duplicated values.
580: * @return The attribute values, or {@code null} if none.
581: */
582: protected int[] getIntegers(final String attribute,
583: final boolean unique) {
584: return (int[]) parseSequence(getString(attribute), unique, true);
585: }
586:
587: /**
588: * Set the attribute to the specified array of values,
589: * or remove the attribute if the array is {@code null}.
590: *
591: * @param attribute The attribute name.
592: * @param value The attribute value.
593: */
594: protected void setIntegers(final String attribute,
595: final int[] values) {
596: setString(attribute, formatSequence(values));
597: }
598:
599: /**
600: * Returns an attribute as a floating point for the {@linkplain #selectChild selected element},
601: * or {@code null} if none. If the attribute can't be parsed as a floating point, then this
602: * method logs a warning and returns {@code null}.
603: *
604: * @param attribute The attribute to fetch (e.g. {@code "minimum"}).
605: * @return The attribute value, or {@code null} if none or unparseable.
606: */
607: protected Double getDouble(final String attribute) {
608: final String value = getString(attribute);
609: if (value != null)
610: try {
611: return Double.valueOf(value);
612: } catch (NumberFormatException e) {
613: warning("getDouble", ErrorKeys.UNPARSABLE_NUMBER_$1,
614: value);
615: }
616: return null;
617: }
618:
619: /**
620: * Set the attribute to the specified floating point value,
621: * or remove the attribute if the value is NaN.
622: *
623: * @param attribute The attribute name.
624: * @param value The attribute value.
625: */
626: protected void setDouble(final String attribute, final double value) {
627: String text = null;
628: if (!Double.isNaN(value) && !Double.isInfinite(value)) {
629: text = Double.toString(value);
630: }
631: setString(attribute, text);
632: }
633:
634: /**
635: * Returns an attribute as an array of floating point for the {@linkplain #selectChild
636: * selected element}, or {@code null} if none. If an element can't be parsed as a floating
637: * point, then this method logs a warning and returns {@code null}.
638: *
639: * @param attribute The attribute to fetch (e.g. {@code "fillValues"}).
640: * @param unique {@code true} if duplicated values should be collapsed into unique values,
641: * or {@code false} for preserving duplicated values.
642: * @return The attribute values, or {@code null} if none.
643: */
644: protected double[] getDoubles(final String attribute,
645: final boolean unique) {
646: return (double[]) parseSequence(getString(attribute), unique,
647: false);
648: }
649:
650: /**
651: * Set the attribute to the specified array of values,
652: * or remove the attribute if the array is {@code null}.
653: *
654: * @param attribute The attribute name.
655: * @param value The attribute value.
656: */
657: protected void setDoubles(final String attribute,
658: final double[] values) {
659: setString(attribute, formatSequence(values));
660: }
661:
662: /**
663: * Implementation of {@link #getIntegers} and {@link #getDoubles} methods.
664: *
665: * @param sequence The sequence to parse.
666: * @param unique {@code true} if duplicated values should be collapsed into unique values,
667: * or {@code false} for preserving duplicated values.
668: * @param integers {@code true} for parsing as {@code int}, or {@code false} for parsing as
669: * {@code double}.
670: * @return The attribute values, or {@code null} if none.
671: */
672: private Object parseSequence(final String sequence,
673: final boolean unique, final boolean integers) {
674: if (sequence == null) {
675: return null;
676: }
677: final Collection/*<Number>*/numbers;
678: if (unique) {
679: numbers = new LinkedHashSet();
680: } else {
681: numbers = new ArrayList();
682: }
683: final StringTokenizer tokens = new StringTokenizer(sequence);
684: while (tokens.hasMoreTokens()) {
685: final String token = tokens.nextToken();
686: final Number number;
687: try {
688: if (integers) {
689: number = Integer.valueOf(token);
690: } else {
691: number = Double.valueOf(token);
692: }
693: } catch (NumberFormatException e) {
694: warning(integers ? "getIntegers" : "getDoubles",
695: ErrorKeys.UNPARSABLE_NUMBER_$1, token);
696: continue;
697: }
698: numbers.add(number);
699: }
700: int count = 0;
701: final Object values;
702: if (integers) {
703: values = new int[numbers.size()];
704: } else {
705: values = new double[numbers.size()];
706: }
707: for (final Iterator it = numbers.iterator(); it.hasNext();) {
708: Array.set(values, count++, it.next());
709: }
710: assert Array.getLength(values) == count;
711: return values;
712: }
713:
714: /**
715: * Formats a sequence for {@link #setIntegers} and {@link #setDoubles} implementations.
716: *
717: * @param value The attribute value.
718: * @return The formatted sequence.
719: */
720: private static String formatSequence(final Object values) {
721: String text = null;
722: if (values != null) {
723: final StringBuffer buffer = new StringBuffer();
724: final int length = Array.getLength(values);
725: for (int i = 0; i < length; i++) {
726: if (i != 0) {
727: buffer.append(' ');
728: }
729: buffer.append(Array.get(values, i));
730: }
731: text = buffer.toString();
732: }
733: return text;
734: }
735:
736: /**
737: * Returns an attribute as a date for the {@linkplain #selectChild selected element},
738: * or {@code null} if none. If the attribute can't be parsed as a date, then this method
739: * logs a warning and returns {@code null}.
740: *
741: * @param attribute The attribute to fetch (e.g. {@code "origin"}).
742: * @return The attribute value, or {@code null} if none or unparseable.
743: */
744: protected Date getDate(final String attribute) {
745: String value = getString(attribute);
746: if (value != null) {
747: value = trimFractionalPart(value);
748: // TODO: remove the cast with J2SE 1.5.
749: return (Date) metadata.dateFormat().parse(value);
750: }
751: return null;
752: }
753:
754: /**
755: * Set the attribute to the specified value, or remove the attribute if the value is null.
756: *
757: * @param attribute The attribute name.
758: * @param value The attribute value.
759: */
760: protected void setDate(final String attribute, final Date value) {
761: String text = null;
762: if (value != null) {
763: text = metadata.dateFormat().format(value);
764: }
765: setString(attribute, text);
766: }
767:
768: /**
769: * Trims the factional part of the given string, provided that it doesn't change the value.
770: * More specifically, this method removes the trailing {@code ".0"} characters if any. This
771: * method is automatically invoked before to {@linkplain #getInteger parse an integer} or to
772: * {@linkplain #getDate parse a date} (for simplifying fractional seconds).
773: *
774: * @param value The value to trim.
775: * @return The value without the trailing {@code ".0"} part.
776: */
777: public static String trimFractionalPart(String value) {
778: value = value.trim();
779: for (int i = value.length(); --i >= 0;) {
780: switch (value.charAt(i)) {
781: case '0':
782: continue;
783: case '.':
784: return value.substring(0, i);
785: default:
786: return value;
787: }
788: }
789: return value;
790: }
791:
792: /**
793: * Convenience method for logging a warning. Do not allow overriding, because
794: * it would not work for warnings emitted by the {@link #getDate} method.
795: */
796: final void warning(final String method, final int key,
797: final Object value) {
798: if (warningsEnabled) {
799: final LogRecord record = Errors.getResources(
800: metadata.getLocale()).getLogRecord(Level.WARNING,
801: key, value);
802: record.setSourceClassName(MetadataAccessor.class.getName());
803: record.setSourceMethodName(method);
804: warningOccurred(record);
805: }
806: }
807:
808: /**
809: * Invoked when a warning occured. This method is invoked when some inconsistency has
810: * been detected in the geographic metadata. The default implementation delegates
811: * to {@link GeographicMetadata#warningOccurred}.
812: */
813: protected void warningOccurred(final LogRecord record) {
814: if (warningsEnabled) {
815: metadata.warningOccurred(record);
816: }
817: }
818:
819: /**
820: * Enables or disables the warnings. Warnings are enabled by default. Subclasses way want
821: * to temporarily disable the warnings when failures are expected as the normal behavior.
822: * For example a subclass may invokes {@link #getInteger} and fallbacks on {@link #getDouble}
823: * if the former failed. In such case, the warnings should be disabled for the integer parsing,
824: * but not for the floating point parsing.
825: *
826: * @param enabled {@code true} for enabling warnings, or {@code false} for disabling.
827: * @return The previous state before this method has been invoked.
828: */
829: protected boolean setWarningsEnabled(final boolean enabled) {
830: final boolean old = warningsEnabled;
831: warningsEnabled = enabled;
832: return old;
833: }
834:
835: /**
836: * Returns a string representation of metadata, mostly for debugging purpose.
837: */
838: public String toString() {
839: return OptionalDependencies.toString(OptionalDependencies
840: .xmlToSwing(parent));
841: }
842: }
|