sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1415754 [2/3] - in /sis/branches/JDK6: ./ ide-project/NetBeans/ ide-project/NetBeans/nbproject/ sis-utility/src/main/java/org/apache/sis/internal/util/ sis-utility/src/main/java/org/apache/sis/io/ sis-utility/src/main/java/org/apache/sis/m...
Date Fri, 30 Nov 2012 17:29:08 GMT
Modified: sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/Numbers.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/Numbers.java?rev=1415754&r1=1415753&r2=1415754&view=diff
==============================================================================
--- sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/Numbers.java (original)
+++ sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/Numbers.java Fri Nov 30 17:29:04 2012
@@ -18,11 +18,20 @@ package org.apache.sis.util;
 
 import java.util.Map;
 import java.util.HashMap;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.Collections;
+import java.lang.reflect.Array;
 import java.math.BigDecimal;
 import java.math.BigInteger;
 
 import org.apache.sis.util.resources.Errors;
 
+import static org.apache.sis.util.collection.Collections.emptyQueue;
+import static org.apache.sis.util.collection.Collections.emptySortedSet;
+
 
 /**
  * Static methods working with {@link Number} objects, and a few primitive types by extension.
@@ -34,7 +43,7 @@ import org.apache.sis.util.resources.Err
  */
 public final class Numbers extends Static {
     /**
-     * Constants to be used in {@code switch} statements.
+     * Constant of value {@value} used in {@code switch} statements or as index in arrays.
      */
     public static final byte
             DOUBLE=8, FLOAT=7, LONG=6, INTEGER=5, SHORT=4, BYTE=3, CHARACTER=2, BOOLEAN=1, OTHER=0;
@@ -62,7 +71,7 @@ public final class Numbers extends Stati
     /** The wrapper for the primitive type.     */ private final Class<?> wrapper;
     /** {@code true} for floating point number. */ private final boolean  isFloat;
     /** {@code true} for integer number.        */ private final boolean  isInteger;
-    /** The size in bytes.                      */ private final byte     size;
+    /** The size in bytes, or -1 if variable.   */ private final byte     size;
     /** Constant to be used in switch statement.*/ private final byte     ordinal;
     /** The internal form of the primitive name.*/ private final char     internal;
     /** The null, NaN, 0 or false value.        */ private final Object   nullValue;
@@ -219,7 +228,7 @@ public final class Numbers extends Stati
      * @throws IllegalArgumentException If a number is not of a known type.
      *
      * @see #widestClass(Number, Number)
-     * @see #finestClass(Number, Number)
+     * @see #narrowestClass(Number, Number)
      */
     public static Class<? extends Number> widestClass(final Number n1, final Number n2)
             throws IllegalArgumentException
@@ -250,7 +259,7 @@ public final class Numbers extends Stati
      * @throws IllegalArgumentException If one of the given types is unknown.
      *
      * @see #widestClass(Class, Class)
-     * @see #finestClass(Number, Number)
+     * @see #narrowestClass(Number, Number)
      */
     public static Class<? extends Number> widestClass(final Class<? extends Number> c1,
                                                       final Class<? extends Number> c2)
@@ -270,27 +279,27 @@ public final class Numbers extends Stati
     }
 
     /**
-     * Returns the finest type of two numbers. Numbers {@code n1} and {@code n2} must be instance
+     * Returns the narrowest type of two numbers. Numbers {@code n1} and {@code n2} must be instance
      * of any of {@link Byte}, {@link Short}, {@link Integer}, {@link Long}, {@link Float}
      * {@link Double}, {@link BigInteger} or {@link BigDecimal} types.
      *
      * @param  n1 The first number.
      * @param  n2 The second number.
-     * @return The finest type of the given numbers.
+     * @return The narrowest type of the given numbers.
      * @throws IllegalArgumentException If a number is not of a known type.
      *
-     * @see #finestClass(Class, Class)
+     * @see #narrowestClass(Class, Class)
      * @see #widestClass(Class, Class)
      */
-    public static Class<? extends Number> finestClass(final Number n1, final Number n2)
+    public static Class<? extends Number> narrowestClass(final Number n1, final Number n2)
             throws IllegalArgumentException
     {
-        return finestClass((n1 != null) ? n1.getClass() : null,
+        return narrowestClass((n1 != null) ? n1.getClass() : null,
                            (n2 != null) ? n2.getClass() : null);
     }
 
     /**
-     * Returns the finest of the given types. Classes {@code c1} and {@code c2} can be
+     * Returns the narrowest of the given types. Classes {@code c1} and {@code c2} can be
      * {@link Byte}, {@link Short}, {@link Integer}, {@link Long}, {@link Float},
      * {@link Double}, {@link BigInteger} or {@link BigDecimal} types.
      *
@@ -300,21 +309,21 @@ public final class Numbers extends Stati
      * Example:
      *
      * {@preformat java
-     *     finestClass(Short.class, Long.class);
+     *     narrowestClass(Short.class, Long.class);
      * }
      *
      * returns {@code Short.class}.
      *
      * @param  c1 The first number type, or {@code null}.
      * @param  c2 The second number type, or {@code null}.
-     * @return The finest of the given types, or {@code null} if both {@code c1} and {@code c2} are null.
+     * @return The narrowest of the given types, or {@code null} if both {@code c1} and {@code c2} are null.
      * @throws IllegalArgumentException If one of the given types is unknown.
      *
-     * @see #finestClass(Number, Number)
+     * @see #narrowestClass(Number, Number)
      * @see #widestClass(Class, Class)
      */
-    public static Class<? extends Number> finestClass(final Class<? extends Number> c1,
-                                                      final Class<? extends Number> c2)
+    public static Class<? extends Number> narrowestClass(final Class<? extends Number> c1,
+                                                         final Class<? extends Number> c2)
             throws IllegalArgumentException
     {
         final Numbers m1 = MAPPING.get(c1);
@@ -333,37 +342,37 @@ public final class Numbers extends Stati
     /**
      * Returns the smallest class capable to hold the specified value. If the given value is
      * {@code null}, then this method returns {@code null}. Otherwise this method delegates
-     * to {@link #finestClass(double)} or {@link #finestClass(long)} depending on the value type.
+     * to {@link #narrowestClass(double)} or {@link #narrowestClass(long)} depending on the value type.
      *
      * @param  value The value to be wrapped in a finer (if possible) {@link Number}.
-     * @return The finest type capable to hold the given value.
+     * @return The narrowest type capable to hold the given value.
      *
-     * @see #finestNumber(Number)
+     * @see #narrowestNumber(Number)
      */
-    public static Class<? extends Number> finestClass(final Number value) {
+    public static Class<? extends Number> narrowestClass(final Number value) {
         if (value == null) {
             return null;
         }
         if (isPrimitiveInteger(value.getClass())) {
-            return finestClass(value.longValue());
+            return narrowestClass(value.longValue());
         } else {
-            return finestClass(value.doubleValue());
+            return narrowestClass(value.doubleValue());
         }
     }
 
     /**
      * Returns the smallest class capable to hold the specified value.
-     * This is similar to {@link #finestClass(long)}, but extended to floating point values.
+     * This is similar to {@link #narrowestClass(long)}, but extended to floating point values.
      *
      * @param  value The value to be wrapped in a {@link Number}.
-     * @return The finest type capable to hold the given value.
+     * @return The narrowest type capable to hold the given value.
      *
-     * @see #finestNumber(double)
+     * @see #narrowestNumber(double)
      */
-    public static Class<? extends Number> finestClass(final double value) {
+    public static Class<? extends Number> narrowestClass(final double value) {
         final long lg = (long) value;
         if (value == lg) {
-            return finestClass(lg);
+            return narrowestClass(lg);
         }
         final float fv = (float) value;
         if (Double.doubleToRawLongBits(value) == Double.doubleToRawLongBits(fv)) {
@@ -387,11 +396,11 @@ public final class Numbers extends Stati
      * </ul>
      *
      * @param  value The value to be wrapped in a {@link Number}.
-     * @return The finest type capable to hold the given value.
+     * @return The narrowest type capable to hold the given value.
      *
-     * @see #finestNumber(long)
+     * @see #narrowestNumber(long)
      */
-    public static Class<? extends Number> finestClass(final long value) {
+    public static Class<? extends Number> narrowestClass(final long value) {
         // Tests MAX_VALUE before MIN_VALUE because it is more likely to fail.
         if (value <= Byte   .MAX_VALUE  &&  value >= Byte   .MIN_VALUE) return Byte.class;
         if (value <= Short  .MAX_VALUE  &&  value >= Short  .MIN_VALUE) return Short.class;
@@ -402,23 +411,23 @@ public final class Numbers extends Stati
     /**
      * Returns the number of the smallest class capable to hold the specified value. If the
      * given value is {@code null}, then this method returns {@code null}. Otherwise this
-     * method delegates to {@link #finestNumber(double)} or {@link #finestNumber(long)}
+     * method delegates to {@link #narrowestNumber(double)} or {@link #narrowestNumber(long)}
      * depending on the value type.
      *
      * @param  value The value to be wrapped in a finer (if possible) {@link Number}.
-     * @return The finest type capable to hold the given value.
+     * @return The narrowest type capable to hold the given value.
      *
-     * @see #finestClass(Number)
+     * @see #narrowestClass(Number)
      */
-    public static Number finestNumber(final Number value) {
+    public static Number narrowestNumber(final Number value) {
         if (value == null) {
             return null;
         }
         final Number candidate;
         if (isPrimitiveInteger(value.getClass())) {
-            candidate = finestNumber(value.longValue());
+            candidate = narrowestNumber(value.longValue());
         } else {
-            candidate = finestNumber(value.doubleValue());
+            candidate = narrowestNumber(value.doubleValue());
         }
         // Keep the existing instance if possible.
         return value.equals(candidate) ? value : candidate;
@@ -426,17 +435,17 @@ public final class Numbers extends Stati
 
     /**
      * Returns the number of the smallest class capable to hold the specified value.
-     * This is similar to {@link #finestNumber(long)}, but extended to floating point values.
+     * This is similar to {@link #narrowestNumber(long)}, but extended to floating point values.
      *
      * @param  value The value to be wrapped in a {@link Number}.
-     * @return The finest type capable to hold the given value.
+     * @return The narrowest type capable to hold the given value.
      *
-     * @see #finestClass(double)
+     * @see #narrowestClass(double)
      */
-    public static Number finestNumber(final double value) {
+    public static Number narrowestNumber(final double value) {
         final long lg = (long) value;
         if (value == lg) {
-            return finestNumber(lg);
+            return narrowestNumber(lg);
         }
         final float fv = (float) value;
         if (Double.doubleToRawLongBits(value) == Double.doubleToRawLongBits(fv)) {
@@ -460,11 +469,11 @@ public final class Numbers extends Stati
      * </ul>
      *
      * @param  value The value to be wrapped in a {@link Number}.
-     * @return The given value as a number of the finest type capable to hold it.
+     * @return The given value as a number of the narrowest type capable to hold it.
      *
-     * @see #finestClass(long)
+     * @see #narrowestClass(long)
      */
-    public static Number finestNumber(final long value) {
+    public static Number narrowestNumber(final long value) {
         // Tests MAX_VALUE before MIN_VALUE because it is more likely to fail.
         if (value <= Byte   .MAX_VALUE  &&  value >= Byte   .MIN_VALUE) return Byte   .valueOf((byte)  value);
         if (value <= Short  .MAX_VALUE  &&  value >= Short  .MIN_VALUE) return Short  .valueOf((short) value);
@@ -476,23 +485,23 @@ public final class Numbers extends Stati
      * Returns the smallest number capable to hold the specified value.
      *
      * @param  value The value to be wrapped in a {@link Number}.
-     * @return The finest type capable to hold the given value.
+     * @return The narrowest type capable to hold the given value.
      * @throws NumberFormatException if the given value can not be parsed as a number.
      *
-     * @see #finestNumber(Number)
-     * @see #finestNumber(double)
-     * @see #finestNumber(long)
+     * @see #narrowestNumber(Number)
+     * @see #narrowestNumber(double)
+     * @see #narrowestNumber(long)
      */
-    public static Number finestNumber(String value) throws NumberFormatException {
-        value = value.trim();
+    public static Number narrowestNumber(String value) throws NumberFormatException {
+        value = CharSequences.trimWhitespaces(value);
         final int length = value.length();
         for (int i=0; i<length; i++) {
             final char c = value.charAt(i);
             if (c == '.' || c == 'e' || c == 'E') {
-                return finestNumber(Double.parseDouble(value));
+                return narrowestNumber(Double.parseDouble(value));
             }
         }
-        return finestNumber(Long.parseLong(value));
+        return narrowestNumber(Long.parseLong(value));
     }
 
     /**
@@ -562,19 +571,12 @@ public final class Numbers extends Stati
      *         string value is not parseable as a number of the specified type.
      */
     @SuppressWarnings("unchecked")
-    public static <T> T valueOf(final Class<T> type, final String value)
+    public static <T> T valueOf(final Class<T> type, String value)
             throws IllegalArgumentException, NumberFormatException
     {
-        if (value == null) {
-            return null;
+        if (value == null || type == String.class) {
+            return (T) value;
         }
-        if (type == Double .class) return (T) Double .valueOf(value);
-        if (type == Float  .class) return (T) Float  .valueOf(value);
-        if (type == Long   .class) return (T) Long   .valueOf(value);
-        if (type == Integer.class) return (T) Integer.valueOf(value);
-        if (type == Short  .class) return (T) Short  .valueOf(value);
-        if (type == Byte   .class) return (T) Byte   .valueOf(value);
-        if (type == Boolean.class) return (T) Boolean.valueOf(value);
         if (type == Character.class) {
             /*
              * If the string is empty, returns 0 which means "end of string" in C/C++
@@ -585,13 +587,75 @@ public final class Numbers extends Stati
              */
             return (T) Character.valueOf(value.isEmpty() ? 0 : value.charAt(0));
         }
-        if (type == String.class) {
-            return (T) value;
-        }
+        value = CharSequences.trimWhitespaces(value);
+        if (type == Double .class) return (T) Double .valueOf(value);
+        if (type == Float  .class) return (T) Float  .valueOf(value);
+        if (type == Long   .class) return (T) Long   .valueOf(value);
+        if (type == Integer.class) return (T) Integer.valueOf(value);
+        if (type == Short  .class) return (T) Short  .valueOf(value);
+        if (type == Byte   .class) return (T) Byte   .valueOf(value);
+        if (type == Boolean.class) return (T) Boolean.valueOf(value);
         throw unknownType(type);
     }
 
     /**
+     * Returns a {@code NaN}, zero, empty or {@code null} value of the given type. This method
+     * tries to return the closest value that can be interpreted as "<cite>none</cite>", which
+     * is usually not the same than "<cite>zero</cite>". More specifically:
+     *
+     * <ul>
+     *   <li>If the given type is a floating point <strong>primitive</strong> type ({@code float}
+     *       or {@code double}), then this method returns {@link Float#NaN} or {@link Double#NaN}
+     *       depending on the given type.</li>
+     *
+     *   <li>If the given type is an integer <strong>primitive</strong> type or the character type
+     *       ({@code long}, {@code int}, {@code short}, {@code byte} or {@code char}), then this
+     *       method returns the zero value of the given type.</li>
+     *
+     *   <li>If the given type is the {@code boolean} <strong>primitive</strong> type, then this
+     *       method returns {@link Boolean#FALSE}.</li>
+     *
+     *   <li>If the given type is an array or a collection, then this method returns an empty
+     *       array or collection. The given type is honored on a <cite>best effort</cite> basis.</li>
+     *
+     *   <li>For all other cases, including the wrapper classes of primitive types, this method
+     *       returns {@code null}.</li>
+     * </ul>
+     *
+     * Despite being defined in the {@code Numbers} class, the scope of this method has been
+     * extended to array and collection types because those objects can also be seen as
+     * mathematical concepts.
+     *
+     * @param  <T> The compile-time type of the requested object.
+     * @param  type The type of the object for which to get a nil value.
+     * @return An object of the given type which represents a nil value, or {@code null}.
+     *
+     * @see org.apache.sis.xml.NilObject
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> T valueOfNil(final Class<T> type) {
+        final Numbers mapping = MAPPING.get(type);
+        if (mapping != null) {
+            if (type.isPrimitive()) {
+                return (T) mapping.nullValue;
+            }
+        } else if (type != null && type != Object.class) {
+            if (type == Map      .class) return (T) Collections.EMPTY_MAP;
+            if (type == List     .class) return (T) Collections.EMPTY_LIST;
+            if (type == Queue    .class) return (T) emptyQueue();
+            if (type == SortedSet.class) return (T) emptySortedSet();
+            if (type.isAssignableFrom(Set.class)) {
+                return (T) Collections.EMPTY_SET;
+            }
+            final Class<?> element = type.getComponentType();
+            if (element != null) {
+                return (T) Array.newInstance(element, 0);
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns one of {@link #DOUBLE}, {@link #FLOAT}, {@link #LONG}, {@link #INTEGER},
      * {@link #SHORT}, {@link #BYTE}, {@link #CHARACTER}, {@link #BOOLEAN} or {@link #OTHER}
      * constants for the given type. This is a commodity for usage in {@code switch} statements.

Modified: sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java?rev=1415754&r1=1415753&r2=1415754&view=diff
==============================================================================
--- sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java (original)
+++ sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java Fri Nov 30 17:29:04 2012
@@ -21,12 +21,19 @@ import java.util.Map;
 import java.util.LinkedHashMap;
 import java.util.Collections;
 import java.io.Serializable;
+import java.text.Format;
 import net.jcip.annotations.NotThreadSafe;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.util.Cloner;
 
+import static org.apache.sis.util.CharSequences.trimWhitespaces;
+import static org.apache.sis.util.collection.Collections.isNullOrEmpty;
 import static org.apache.sis.util.collection.Collections.hashMapCapacity;
 
+// Related to JDK7
+import org.apache.sis.internal.util.Objects;
+
 
 /**
  * A {@link TreeTable} implementation with a {@linkplain #getColumns() list of columns} given at
@@ -36,12 +43,12 @@ import static org.apache.sis.util.collec
  * Example:
  *
  * {@preformat java
- *     class CityLocation {
- *         public static final TableColumn<String> CITY_NAME  = new MyColumn<>(String.class);
- *         public static final TableColumn<Float>  LATITUDE   = new MyColumn<>(Float .class);
- *         public static final TableColumn<Float>  LONGTITUDE = new MyColumn<>(Float .class);
+ *     public class CityLocation {
+ *         public static final TableColumn<String> CITY_NAME  = new TableColumn<>(String.class);
+ *         public static final TableColumn<Float>  LATITUDE   = new TableColumn<>(Float .class);
+ *         public static final TableColumn<Float>  LONGTITUDE = new TableColumn<>(Float .class);
  *
- *         TreeTable createTable() {
+ *         public TreeTable createTable() {
  *             DefaultTreeTable table = new DefaultTreeTable(CITY_NAME, LATITUDE, LONGITUDE);
  *             TreeTable.Node   city  = new DefaultTreeTable.Node(table);
  *             city.setValue(CITY_NAME, "Rimouski");
@@ -63,13 +70,18 @@ import static org.apache.sis.util.collec
  * @module
  */
 @NotThreadSafe
-public class DefaultTreeTable implements TreeTable, Serializable {
+public class DefaultTreeTable implements TreeTable, Cloneable, Serializable {
     /**
      * For cross-version compatibility.
      */
     private static final long serialVersionUID = 1951201018202846555L;
 
     /**
+     * Shared {@code TreeTableFormat} instance for {@link #toString()} implementation.
+     */
+    private static Format format;
+
+    /**
      * The root node, or {@code null} if not yet specified.
      *
      * @see #getRoot()
@@ -112,7 +124,7 @@ public class DefaultTreeTable implements
      * <p>The {@linkplain #getRoot() root} node is initially {@code null}. Callers can initialize
      * it after construction time by a call to the {@link #setRoot(TreeTable.Node)} method.</p>
      *
-     * @param columns The table columns.
+     * @param columns The list of table columns.
      */
     public DefaultTreeTable(TableColumn<?>... columns) {
         ArgumentChecks.ensureNonNull("columns", columns);
@@ -174,6 +186,9 @@ public class DefaultTreeTable implements
     /**
      * Returns the table columns given at construction time.
      * The returned list is never null neither empty.
+     *
+     * @see Node#getValue(TableColumn)
+     * @see Node#setValue(TableColumn, Object)
      */
     @Override
     public final List<TableColumn<?>> getColumns() {
@@ -218,8 +233,61 @@ public class DefaultTreeTable implements
     }
 
     /**
+     * Returns a clone of this table. This method clones the {@linkplain #getRoot() root} node.
+     * If the root is an instance of {@link Node}, then cloning the root will recursively clone
+     * all its {@linkplain Node#getChildren() children}.
+     *
+     * @return A clone of this table.
+     * @throws CloneNotSupportedException If this table, the root node or one of its children
+     *         can not be cloned.
+     *
+     * @see Node#clone()
+     */
+    @Override
+    public DefaultTreeTable clone() throws CloneNotSupportedException {
+        final DefaultTreeTable clone = (DefaultTreeTable) super.clone();
+        clone.root = (TreeTable.Node) new Cloner().clone(clone.root);
+        return clone;
+    }
+
+    /**
+     * Compares the given object with this tree table for equality. This method compares the
+     * {@linkplain #getColumns() columns} and the {@linkplain #getRoot() root node}. If the
+     * later is an instance of the {@link Node} inner class, then all node values and children
+     * will be compared recursively.
+     *
+     * @param  other The object to compare with this table.
+     * @return {@code true} if the two objects are equal.
+     *
+     * @see Node#equals(Object)
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (other == this) {
+            return true;
+        }
+        if (other != null && other.getClass() == getClass()) {
+            final DefaultTreeTable that = (DefaultTreeTable) other;
+            return columnIndices.equals(that.columnIndices) &&
+                    Objects.equals(root, that.root);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code value for this table.
+     * This method is defined for consistency with {@link #equals(Object)} contract.
+     *
+     * @see Node#hashCode()
+     */
+    @Override
+    public int hashCode() {
+        return (columnIndices.hashCode() + 31*Objects.hashCode(root)) ^ (int) serialVersionUID;
+    }
+
+    /**
      * Returns a string representation of this tree table.
-     * The default implementation delegates to {@link TreeTables#toString(TreeTable)}.
+     * The default implementation delegates to {@link #toString(TreeTable)}.
      * This is okay for debugging or occasional usages. However for more extensive usages,
      * developers are encouraged to create and configure their own {@link TreeTableFormat}
      * instance.
@@ -228,7 +296,25 @@ public class DefaultTreeTable implements
      */
     @Override
     public String toString() {
-        return TreeTables.toString(this);
+        return toString(this);
+    }
+
+    /**
+     * Returns a string representation of the given tree table.
+     * The default implementation uses a shared instance of {@link TreeTableFormat}.
+     * This is okay for debugging or occasional usages. However for more extensive usages,
+     * developers are encouraged to create and configure their own {@code TreeTableFormat}
+     * instance.
+     *
+     * @param  table The tree table to format.
+     * @return A string representation of the given tree table.
+     */
+    public static synchronized String toString(final TreeTable table) {
+        ArgumentChecks.ensureNonNull("table", table);
+        if (format == null) {
+            format = new TreeTableFormat(null, null);
+        }
+        return format.format(table);
     }
 
 
@@ -239,9 +325,12 @@ public class DefaultTreeTable implements
      * of columns. The list of columns is specified by a {@link TreeTable}, or inherited from
      * a parent node.
      *
-     * <p>The {@linkplain #getChildren() list of children} provided by this class is <cite>live</cite>:
-     * adding a {@code Node} child to that list will automatically set its parent to {@code this},
-     * and removing a {@code Node} from that list will set its parent to {@code null}.</p>
+     * {@section Note on the parent node}
+     * The value returned by the {@link #getParent()} method is updated automatically when
+     * this node is <em>added to</em> or <em>removed from</em> the {@linkplain #getChildren()
+     * list of children} of another {@code Node} instance - there is no {@code setParent(Node)}
+     * method. As a derived value, the parent is ignored by the {@link #clone()},
+     * {@link #equals(Object)} and {@link #hashCode()} methods.
      *
      * @author  Martin Desruisseaux (Geomatys)
      * @since   0.3
@@ -249,7 +338,7 @@ public class DefaultTreeTable implements
      * @module
      */
     @NotThreadSafe
-    public static class Node implements TreeTable.Node, Serializable {
+    public static class Node implements TreeTable.Node, Cloneable, Serializable {
         /**
          * For cross-version compatibility.
          */
@@ -329,14 +418,6 @@ public class DefaultTreeTable implements
         private Object[] values;
 
         /**
-         * Creates a new node with the given shared map of columns and the given values.
-         */
-        Node(final Map<TableColumn<?>,Integer> columnIndices, final Object[] values) {
-            this.columnIndices = columnIndices;
-            this.values = values;
-        }
-
-        /**
          * Creates a new node for the given table. The new node will be able to store a value
          * for each {@linkplain TreeTable#getColumns() columns} defined in the given table.
          *
@@ -390,9 +471,30 @@ public class DefaultTreeTable implements
         }
 
         /**
+         * Creates a node with a single column for object names (c<cite>convenience constructor</cite>).
+         * The node will have the following columns:
+         *
+         * <table class="sis">
+         *   <tr><th>Header</th> <th>Type</th>                 <th>Initial value</th></tr>
+         *   <tr><td>"Name"</td> <td>{@link CharSequence}</td> <td>{@code name}</td></tr>
+         * </table>
+         *
+         * @param  name The initial value for the "Name" column (can be {@code null}).
+         */
+        public Node(final CharSequence name) {
+            columnIndices = TableColumn.NAME_MAP;
+            if (name != null) {
+                values = new CharSequence[] {name};
+            }
+        }
+
+        /**
          * Returns the parent of this node. On {@code Node} creation, this value may be initially
          * {@code null}. It will be automatically set to a non-null value when this node will be
          * added as a child of another {@code Node} instance.
+         *
+         * <p>Note that the parent is intentionally ignored by the {@link #clone()},
+         * {@link #equals(Object)} and {@link #hashCode()} methods.</p>
          */
         @Override
         public final TreeTable.Node getParent() {
@@ -495,23 +597,134 @@ public class DefaultTreeTable implements
         }
 
         /**
-         * Returns a string representation of this node, for identification in error message
-         * or in debugger.
+         * Returns a clone of this node without parent.
+         * This method recursively clones all {@linkplain #getChildren() children},
+         * but does not clone the column {@linkplain #getValue(TableColumn) values}.
+         * The parent of the cloned node is set to {@code null}.
+         *
+         * @return A clone of this node without parent.
+         * @throws CloneNotSupportedException If this node or one of its children can not be cloned.
+         */
+        @Override
+        public Node clone() throws CloneNotSupportedException {
+            final Node clone = (Node) super.clone();
+            clone.parent = null;
+            if (clone.values != null) {
+                clone.values = clone.values.clone();
+            }
+            if (clone.children != null) {
+                clone.children = new Children(clone);
+                for (final TreeTable.Node child : children) {
+                    /*
+                     * Implementation note: we could have used the Cloner for cloning arbitrary
+                     * node implementations, but children.add(...) would fail anyway because it
+                     * can not set the parent of unknown implementation.
+                     */
+                    if (!(child instanceof Node)) {
+                        throw new CloneNotSupportedException(Errors.format(
+                                Errors.Keys.CloneNotSupported_1, child.getClass()));
+                    }
+                    clone.children.add(((Node) child).clone());
+                }
+            }
+            return clone;
+        }
+
+        /**
+         * Compares the given object with this node for {@linkplain #getValue(TableColumn) values}
+         * and {@linkplain #getChildren() children} equality, ignoring the {@linkplain #getParent()
+         * parent}. This method can be used for determining if two branches of a same tree or of two
+         * different trees are identical.
+         *
+         * {@note This method ignores the parent because:
+         * <ul>
+         *   <li>When comparing the children recursively, comparing the parents would cause infinite recursivity.</li>
+         *   <li>For consistency with the <code>clone()</code> method, which can not clone the parent.</li>
+         *   <li>For making possible to compare branches instead than only whole trees.</li>
+         * </ul>}
+         *
+         * @param  other The object to compare with this node.
+         * @return {@code true} if the two objects are equal, ignoring the parent node.
+         */
+        @Override
+        public boolean equals(final Object other) {
+            if (other == this) {
+                return true;
+            }
+            if (other != null && other.getClass() == getClass()) {
+                final Node that = (Node) other;
+                if (columnIndices.equals(that.columnIndices)) {
+                    final Object[] v1 = this.values;
+                    final Object[] v2 = that.values;
+                    if (v1 != v2) { // For skipping the loop if v1 and v2 are null.
+                        for (int i=columnIndices.size(); --i>=0;) {
+                            if (!Objects.equals((v1 != null) ? v1[i] : null,
+                                                (v2 != null) ? v2[i] : null))
+                            {
+                                return false;
+                            }
+                        }
+                    }
+                    final List<TreeTable.Node> c1 = this.children;
+                    final List<TreeTable.Node> c2 = that.children;
+                    final int n = (c1 != null) ? c1.size() : 0;
+                    if (((c2 != null) ? c2.size() : 0) == n) {
+                        for (int i=0; i<n; i++) {
+                            if (!c1.get(i).equals(c2.get(i))) {
+                                return false;
+                            }
+                        }
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Returns a hash-code value computed from the {@linkplain #getValue(TableColumn) values}
+         * and {@linkplain #getChildren() children}, ignoring the {@linkplain #getParent() parent}.
+         * This method is defined for consistency with {@link #equals(Object)} contract.
+         */
+        @Override
+        public int hashCode() {
+            int hash = 0;
+            final Object[] values = this.values;
+            if (values != null) {
+                // Do not use Objects.hashCode(...) because we want the result of array
+                // containing only null elements to be the same than null array (zero).
+                for (int i=values.length; --i>=0;) {
+                    hash = 31*hash + Objects.hash(values[i]);
+                }
+            }
+            // Do not use Objects.hashCode(...) because we
+            // want the same result for null and empty list.
+            if (!isNullOrEmpty(children)) {
+                hash += 37 * children.hashCode();
+            }
+            return hash ^ (int) serialVersionUID;
+        }
+
+        /**
+         * Returns a string representation of this node for identification in error message or in debugger.
+         * The default implementation returns the {@code toString()} value of the first non-empty
+         * {@link CharSequence} found in the {@linkplain #getValue(TableColumn) values}, if any.
+         * If no such value is found, then this method returns "<var>Node</var>-<var>i</var>"
+         * where <var>Node</var> is the {@linkplain Class#getSimpleName() simple classname}
+         * and <var>i</var> is the index of this node in the parent node.
          *
          * @return A string representation of this node.
          */
         @Override
         public String toString() {
-            Object value = getUserObject();
-            if (value instanceof CharSequence) {
-                return value.toString();
-            }
             final Object[] values = this.values;
             if (values != null) {
-                for (int i=0; i<values.length; i++) {
-                    value = values[i];
+                for (final Object value : values) {
                     if (value instanceof CharSequence) {
-                        return value.toString();
+                        final String text = trimWhitespaces(value.toString());
+                        if (text != null && !text.isEmpty()) {
+                            return text;
+                        }
                     }
                 }
             }

Modified: sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TableColumn.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TableColumn.java?rev=1415754&r1=1415753&r2=1415754&view=diff
==============================================================================
--- sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TableColumn.java (original)
+++ sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TableColumn.java Fri Nov 30 17:29:04 2012
@@ -16,7 +16,14 @@
  */
 package org.apache.sis.util.collection;
 
+import java.util.Map;
+import java.util.Collections;
+import java.io.Serializable;
+import java.io.InvalidObjectException;
 import org.opengis.util.InternationalString;
+import org.apache.sis.util.type.Types;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Vocabulary;
 
 
 /**
@@ -26,23 +33,63 @@ import org.opengis.util.InternationalStr
  * as in the following example:
  *
  * {@preformat java
- *     class CityLocation {
- *         public static final TableColumn<String> CITY_NAME  = new MyColumn<>(String.class);
- *         public static final TableColumn<Float>  LATITUDE   = new MyColumn<>(Float .class);
- *         public static final TableColumn<Float>  LONGTITUDE = new MyColumn<>(Float .class);
+ *     public class CityLocation {
+ *         public static final ColumnTable<String> CITY_NAME = new ColumnTable<>(String.class, "City name");
+ *         public static final ColumnTable<Float>  LATITUDE  = new ColumnTable<>(Float.class,  "Latitude");
+ *         public static final ColumnTable<Float>  LONGITUDE = new ColumnTable<>(Float.class,  "Longitude");
  *
- *         private String city;
+ *         private String name;
  *         private float  latitude;
  *         private float  longitude;
  *
  *         CityLocation(TreeTable.Node myNode) {
- *             city      = myNode.getValue(CITY_NAME);
- *             latitude  = myNode.getValue(LATITUDE );
+ *             name      = myNode.getValue(CITY_NAME);
+ *             latitude  = myNode.getValue(LATITUDE);
  *             longitude = myNode.getValue(LONGITUDE);
  *         }
  *     }
  * }
  *
+ * {@section Identity comparisons and serialization}
+ * This base class relies on <cite>identity comparisons</cite> instead than defining the
+ * {@code equals(Object)} method, because the {@linkplain #getElementType() element type}
+ * is not a sufficient criterion for differentiating the columns (many columns have values
+ * of the same type) and the {@linkplain #getHeader() header} is arbitrary. Consequently
+ * developers who create their own instances are encouraged to declare them as static final
+ * constants as in the above example, and use those constants consistently.
+ *
+ * <p>This base class is not serializable because the default deserialization mechanism does
+ * not resolve automatically the deserialized instances to the above-cited singleton instances.
+ * Developers who need serialization support for their own instances have to resolve them in
+ * their own subclass. The following example is one possible way to achieve that goal:</p>
+ *
+ * {@preformat java
+ *     public class CityLocation {
+ *         public static final ColumnTable<String> CITY_NAME = new Column<>("CITY_NAME", String.class, "City name");
+ *         public static final ColumnTable<Float>  LATITUDE  = new Column<>("LATITUDE",  Float.class,  "Latitude");
+ *         public static final ColumnTable<Float>  LONGITUDE = new Column<>("LONGITUDE", Float.class,  "Longitude");
+ *
+ *         private static final class Column<V> extends TableColumn<V> implements Serializable {
+ *             private final String field;
+ *
+ *             private Column(String field, Class<V> type, CharSequence header) {
+ *                 super(type, header);
+ *                 this.field = field;
+ *             }
+ *
+ *             private Object readResolve() throws InvalidObjectException {
+ *                 try {
+ *                     return CityLocation.class.getField(field).get(null);
+ *                 } catch (Exception cause) { // Many exceptions, including unchecked ones.
+ *                     throw new InvalidObjectException(cause.toString());
+ *                 }
+ *             }
+ *         }
+ *     }
+ * }
+ *
+ * The constants defined in this class use a similar approach for providing serialization support.
+ *
  * @param <V> Base type of all values in the column identified by this instance.
  *
  * @author  Martin Desruisseaux (Geomatys)
@@ -50,18 +97,168 @@ import org.opengis.util.InternationalStr
  * @version 0.3
  * @module
  */
-public interface TableColumn<V> extends CheckedContainer<V> {
+public class TableColumn<V> implements CheckedContainer<V> {
+    /**
+     * Frequently-used constant for a column of object names.
+     * The values are typically instances of {@link String} or {@link InternationalString},
+     * depending on whether the data provide localization support or not.
+     */
+    public static final TableColumn<CharSequence> NAME = new Constant<CharSequence>("NAME",
+            CharSequence.class, Vocabulary.Keys.Name);
+
+    /**
+     * Frequently-used constant for a column of object types.
+     */
+    @SuppressWarnings("unchecked")
+    public static final TableColumn<Class<?>> TYPE = new Constant<Class<?>>("TYPE",
+            (Class) Class.class, Vocabulary.Keys.Type);
+
+    /**
+     * A map containing only the {@link #NAME} column.
+     * This is the default set of columns when parsing a table tree.
+     */
+    static final Map<TableColumn<?>,Integer> NAME_MAP =
+            Collections.<TableColumn<?>,Integer>singletonMap(NAME, 0);
+
+    /**
+     * Base type of all values in the column identified by this {@code ColumnTable} instance.
+     */
+    private final Class<V> type;
+
+    /**
+     * The column header, or {@code null} if not yet created.
+     */
+    CharSequence header;
+
+    /**
+     * Implementation of {@link TableColumn} for the pre-defined constants.
+     * This implementation differs resource bundle loading until first needed,
+     * and resolves deserialized instances to the singleton instances.
+     *
+     * @param <V> Base type of all values in the column identified by this instance.
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @since   0.3
+     * @version 0.3
+     * @module
+     */
+    private static final class Constant<V> extends TableColumn<V> implements Serializable {
+        /**
+         * For cross-version compatibility.
+         */
+        private static final long serialVersionUID = -2486202389234601560L;
+
+        /**
+         * The programmatic name of the static final field holding this constant.
+         */
+        private final String field;
+
+        /**
+         * The resource key for the column header.
+         */
+        private final transient int resourceKey;
+
+        /**
+         * Creates a new instance for a build-in constant.
+         *
+         * @param field  The programmatic name of the static final field holding this constant.
+         * @param type   Base type of all values in the column identified by this instance.
+         * @param header The resource key for the column header.
+         */
+        Constant(final String field, final Class<V> type, final int header) {
+            super(type);
+            this.field       = field;
+            this.resourceKey = header;
+        }
+
+        /**
+         * Returns the text to display as column header.
+         */
+        @Override
+        public synchronized InternationalString getHeader() {
+            InternationalString i18n = (InternationalString) header;
+            if (i18n == null) {
+                header = i18n = Vocabulary.formatInternational(resourceKey);
+            }
+            return i18n;
+        }
+
+        /**
+         * Invoked on deserialization for resolving this instance to one of the predefined constants.
+         *
+         * @return One of the predefined constants.
+         * @throws InvalidObjectException If this instance can not be resolved.
+         */
+        private Object readResolve() throws InvalidObjectException {
+            try {
+                return TableColumn.class.getField(field).get(null);
+            } catch (Exception cause) { // Many exceptions, including unchecked ones.
+                InvalidObjectException e = new InvalidObjectException(cause.toString());
+                e.initCause(cause);
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * Invoked on deserialization for creating an initially empty instance.
+     * This constructor has {@code protected} visibility only because the Java deserialization
+     * mechanism requires so; this constructor shall not be invoked in any other context.
+     * See the <cite>Identity comparisons and serialization</cite> section in the class
+     * javadoc for more information.
+     */
+    protected TableColumn() {
+        type = null;
+    }
+
+    /**
+     * Creates a new instance for a build-in constant.
+     *
+     * @param type Base type of all values in the column identified by this instance.
+     */
+    TableColumn(final Class<V> type) {
+        this.type = type;
+    }
+
+    /**
+     * Creates a new instance for the given type of values.
+     *
+     * @param type   Base type of all values in the column identified by this instance.
+     * @param header The text to display as column header.
+     */
+    public TableColumn(final Class<V> type, final CharSequence header) {
+        ArgumentChecks.ensureNonNull("type",   this.type   = type);
+        ArgumentChecks.ensureNonNull("header", this.header = header);
+        this.header = Types.toInternationalString(header);
+    }
+
     /**
      * Returns the text to display as column header.
      *
      * @return The text to display as column header.
      */
-    InternationalString getHeader();
+    public synchronized InternationalString getHeader() {
+        final InternationalString i18n = Types.toInternationalString(header);
+        header = i18n;
+        return i18n;
+    }
 
     /**
      * Returns the base type of all values in any column identified by this {@code TableColumn}
      * instance.
      */
     @Override
-    Class<V> getElementType();
+    public final Class<V> getElementType() {
+        return type;
+    }
+
+    /**
+     * Returns a string representation of this table column.
+     * The default implementation returns the {@linkplain #getHeader() header}
+     * in its default locale.
+     */
+    @Override
+    public String toString() {
+        return String.valueOf(getHeader());
+    }
 }

Modified: sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTable.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTable.java?rev=1415754&r1=1415753&r2=1415754&view=diff
==============================================================================
--- sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTable.java (original)
+++ sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTable.java Fri Nov 30 17:29:04 2012
@@ -20,10 +20,46 @@ import java.util.List;
 
 
 /**
- * The root of a tree of nodes, together with the definition of table columns.
- * The {@link #getColumns()} method gives the list of all columns that can be found
- * in a {@code TreeTable}. Usually some or all of those columns are also available as
- * {@link TableColumn} constants defined in {@link TreeTables}.
+ * Defines the structure (list of columns) of a table and provides the root of the tree
+ * containing the data. {@code TreeTable} can be seen as a table in which the first
+ * column contains a tree. Every row in this table is a {@link Node} instance, and each
+ * node can have an arbitrary number of {@linkplain Node#getChildren() children} nodes.
+ *
+ * <p>Below is an example of what a two-columns {@code TreeTable} instance may look like
+ * when {@linkplain TreeTableFormat formatted as a text}:</p>
+ *
+ * {@preformat text
+ *   Citation
+ *   ├───Title…………………………………………………………… Open Geospatial Consortium
+ *   ├───Presentation Forms………………………… document digital
+ *   ├───Cited Responsible Parties
+ *   │   ├───Organisation Name………………… Open Geospatial Consortium
+ *   │   ├───Role…………………………………………………… resource provider
+ *   │   └───Contact Info
+ *   │       └───Online Resource
+ *   │           ├───Linkage……………………… http://www.opengeospatial.org/
+ *   │           └───Function…………………… information
+ *   └───Identifiers
+ *       └───Code…………………………………………………… OGC
+ * }
+ *
+ * <p>In many cases, the columns are known in advance as hard-coded static constants.
+ * Those column constants are typically documented close to the class producing the
+ * {@code TreeTable} instance. Using directly those static constants provides type
+ * safety, as in the following example:</p>
+ *
+ * {@preformat java
+ *     TreeTable table = ...; // Put here a TreeTable instance.
+ *     TreeTable.Node node = table.getRoot();
+ *     CharSequence   name = node.getValue(TableColumn.NAME);
+ *     Class<?>       type = node.getValue(TableColumn.TYPE);
+ * }
+ *
+ * In the above example, the type of value returned by the {@link Node#getValue(TableColumn)}
+ * method is determined by the column constant. However this approach is possible only when
+ * the table structure is known in advance. If a method needs to work with arbitrary tables,
+ * then that method can get the list of columns by a call to {@link #getColumns()}. However
+ * this column list does not provide the above type-safety.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.3
@@ -40,6 +76,7 @@ public interface TreeTable {
      * @return The union of all table columns in every tree node.
      *
      * @see Node#getValue(TableColumn)
+     * @see Node#setValue(TableColumn, Object)
      */
     List<TableColumn<?>> getColumns();
 
@@ -84,6 +121,11 @@ public interface TreeTable {
         /**
          * Returns the parent node, or {@code null} if this node is the root of the tree.
          *
+         * <p>There is intentionally no {@code setParent(Node)} method, as children and parent
+         * managements are highly implementation-dependant. If the {@linkplain #getChildren()
+         * children list} is modifiable, then implementations are encouraged to update automatically
+         * the parent when a child is <em>added to</em> or <em>removed from</em> the children list.</p>
+         *
          * @return The parent, or {@code null} if none.
          * @category tree
          */
@@ -93,6 +135,12 @@ public interface TreeTable {
          * Returns the children of this node. The returned list may or may not be modifiable, at
          * implementation choice. If the list is modifiable, then it shall be <cite>live</cite>,
          * i.e. any modification to the returned list are reflected immediately in the tree.
+         * This allows addition or removal of child nodes as below:
+         *
+         * {@preformat java
+         *     TreeTable.Node newNode = new ...; // Create a new node here.
+         *     parent.getChildren().add(newNode);
+         * }
          *
          * @return The children, or an empty list if none.
          * @category tree

Modified: sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java?rev=1415754&r1=1415753&r2=1415754&view=diff
==============================================================================
--- sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java (original)
+++ sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java Fri Nov 30 17:29:04 2012
@@ -25,12 +25,15 @@ import java.io.IOException;
 import java.text.Format;
 import java.text.ParsePosition;
 import java.text.ParseException;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
 import net.jcip.annotations.NotThreadSafe;
 import org.apache.sis.io.LineFormatter;
 import org.apache.sis.io.TableFormatter;
 import org.apache.sis.io.CompoundFormat;
 import org.apache.sis.util.Workaround;
 import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.StringBuilders;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.internal.util.LocalizedParseException;
@@ -114,12 +117,35 @@ public class TreeTableFormat extends Com
     private int verticalLinePosition;
 
     /**
+     * The string to write before and after the {@link #columnSeparator},
+     * or an empty string if none.
+     */
+    String separatorPrefix, separatorSuffix;
+
+    /**
      * The column separator to use at formatting time if there is more than one column.
+     * This character will be repeated as many time as needed.
      *
-     * @see #getColumnSeparator()
-     * @see #setColumnSeparator(String)
+     * @see #getColumnSeparatorPattern()
+     * @see #setColumnSeparatorPattern(String)
+     */
+    char columnSeparator;
+
+    /**
+     * {@code true} if the trailing {@code null} values shall be omitted at formatting time.
      */
-    String columnSeparator;
+    boolean omitTrailingNulls;
+
+    /**
+     * {@code true} if the user defined the parsing pattern explicitely.
+     */
+    boolean isParsePatternDefined;
+
+    /**
+     * The pattern used at parsing time for finding the column separators, or {@code null}
+     * if not yet constructed. This field is serialized because it may be a user-specified pattern.
+     */
+    private Pattern parsePattern;
 
     /**
      * The line separator to use for formatting the tree.
@@ -153,9 +179,12 @@ public class TreeTableFormat extends Com
      */
     public TreeTableFormat(final Locale locale, final TimeZone timezone) {
         super(locale, timezone);
-        indentation     = 4;
-        columnSeparator = "……";
-        lineSeparator   = JDK7.lineSeparator();
+        indentation       = 4;
+        separatorPrefix   = "……";
+        columnSeparator   = '…';
+        separatorSuffix   = " ";
+        omitTrailingNulls = true;
+        lineSeparator     = JDK7.lineSeparator();
     }
 
     /**
@@ -214,41 +243,149 @@ public class TreeTableFormat extends Com
     }
 
     /**
-     * Returns the characters used in column separators. This character will be used
-     * only if more than one column is formatted. The default value is {@code "……"}.
-     *
-     * @return The current column separator.
-     */
-    public String getColumnSeparator() {
-        return columnSeparator;
+     * Returns the pattern of characters used in column separators. Those characters will be used
+     * only if more than one column is formatted. See {@link #setColumnSeparatorPattern(String)}
+     * for a description of the pattern syntax.
+     *
+     * <p>The default pattern is {@code "?……[…] "}, which means "<cite>If the next value is
+     * non-null, then insert the {@code "……"} string, repeat the {@code '…'} character as many
+     * time as needed (may be zero), and finally insert a space</cite>".</p>
+     *
+     * @return The pattern of the current column separator.
+     */
+    public String getColumnSeparatorPattern() {
+        final StringBuilder buffer = new StringBuilder(8);
+        buffer.append(separatorPrefix).append('\uFFFF').append(separatorSuffix);
+        StringBuilders.replace(buffer, "\\", "\\\\");
+        StringBuilders.replace(buffer, "?",  "\\?");
+        StringBuilders.replace(buffer, "[",  "\\[");
+        StringBuilders.replace(buffer, "]",  "\\]");
+        StringBuilders.replace(buffer, "/",  "\\/");
+        if (omitTrailingNulls) {
+            buffer.insert(0, '?');
+        }
+        final int insertAt = buffer.indexOf("\uFFFF");
+        buffer.replace(insertAt, insertAt+1, "[\uFFFF]").setCharAt(insertAt+1, columnSeparator);
+        if (isParsePatternDefined) {
+            buffer.append('/').append(parsePattern.pattern());
+        }
+        return buffer.toString();
     }
 
     /**
-     * Sets the characters to insert between the columns. The separator shall be non-empty and
-     * contains at least one non-space character. The last character will be repeated as many
-     * time as needed for columns alignment.
+     * Sets the pattern of the characters to insert between the columns. The pattern shall contain
+     * exactly one occurrence of the {@code "[ ]"} pair of bracket, with exactly one character
+     * between them. This character will be repeated as many time as needed for columns alignment.
+     *
+     * <p>The formatting pattern can optionally be followed by a regular expression to be used at
+     * parsing time. If omitted, the parsing pattern will be inferred from the formatting pattern.
+     * If specified, then the {@link #parse(CharSequence, ParsePosition) parse} method will invoke
+     * the {@link Matcher#find()} method for determining the column boundaries.</p>
+     *
+     * <p>The characters listed below have special meaning in the pattern.
+     * Other characters are appended <cite>as-is</cite> between the columns.</p>
+     *
+     * <table class="sis">
+     *   <tr><th>Character(s)</th> <th>Meaning</th></tr>
+     *   <tr><td>{@code '?'}</td>  <td>Omit the column separator for trailing null values.</td></tr>
+     *   <tr><td>{@code "[ ]"}</td><td>Repeat the character between bracket as needed.</td></tr>
+     *   <tr><td>{@code '/'}</td>  <td>Separate the formatting pattern from the parsing pattern.</td></tr>
+     *   <tr><td>{@code '\\'}</td> <td>Escape any of the characters listed in this table.</td></tr>
+     * </table>
      *
-     * <p>In current implementation, the above-cited repeated character must be in the
-     * {@linkplain Character#isBmpCodePoint(int) Basic Multilanguage Plane}.</p>
+     * Restrictions:
+     * <ul>
+     *   <li>If present, {@code '?'} shall be the first character in the pattern.</li>
+     *   <li>The repeated character (specified inside the pair of brackets) is mandatory.</li>
+     *   <li>In the current implementation, the repeated character must be in the
+     *       {@linkplain Character#isBmpCodePoint(int) Basic Multilanguage Plane}.</li>
+     *   <li>If {@code '/'} is present, anything on its right must be compliant
+     *       with the {@link Pattern} syntax.</li>
+     * </ul>
      *
-     * @param  separator The new column separator.
-     * @throws IllegalArgumentException If the given separator does not contains at least one
-     *         non-space character, or the last character is not in the BMP plane.
+     * @param  pattern The pattern of the new column separator.
+     * @throws IllegalArgumentException If the given pattern is illegal.
      */
-    public void setColumnSeparator(final String separator) throws IllegalArgumentException {
-        ArgumentChecks.ensureNonEmpty("separator", separator);
-        final int length = separator.length();
-        if (JDK7.isBmpCodePoint(separator.codePointBefore(length))) {
-            for (int i=0; i<length; i++) {
-                final int c = separator.codePointAt(i);
-                if (!Character.isSpaceChar(c) && !Character.isISOControl(c)) {
-                    columnSeparator = separator;
-                    return;
+    public void setColumnSeparatorPattern(final String pattern) throws IllegalArgumentException {
+        ArgumentChecks.ensureNonEmpty("pattern", pattern);
+        final int length = pattern.length();
+        final StringBuilder buffer = new StringBuilder(length);
+        boolean escape  = false;
+        boolean trim    = false;
+        String  prefix  = null;
+        String  regex   = null;
+        int separatorIndex = -1;
+scan:   for (int i=0; i<length; i++) {
+            final char c = pattern.charAt(i);
+            switch (c) {
+                case '\uFFFF': { // This "character" is reserved.
+                    prefix = null;
+                    break scan; // This will cause IllegalArgumentException to be thrown.
+                }
+                case '\\': {
+                    if (i != separatorIndex) {
+                        if (escape) break;
+                        escape = true;
+                    }
+                    continue;
+                }
+                case '?': {
+                    if (i != 0) {
+                        prefix = null;
+                        break scan;
+                    }
+                    trim = true;
+                    continue;
+                }
+                case '[': {
+                    if (escape) break;
+                    if (i != separatorIndex) {
+                        if (separatorIndex >= 0) {
+                            prefix = null;
+                            break scan; // This will cause IllegalArgumentException to be thrown.
+                        }
+                        separatorIndex = i+1;
+                    }
+                    continue;
+                }
+                case ']': {
+                    if (escape) break;
+                    switch (i - separatorIndex) {
+                        case 0:  continue;
+                        case 1:  prefix = buffer.toString(); buffer.setLength(0); continue;
+                        default: prefix = null; break scan;
+                    }
                 }
+                case '/': {
+                    if (escape) break;
+                    regex = pattern.substring(i+1);
+                    break scan;
+                }
+            }
+            if (i != separatorIndex) {
+                buffer.append(c);
             }
         }
-        throw new IllegalArgumentException(Errors.format(
-                Errors.Keys.IllegalArgumentValue_2, "separator", separator));
+        if (prefix == null) {
+            throw new IllegalArgumentException(Errors.format(
+                    Errors.Keys.IllegalFormatPatternForClass_2, pattern, TreeTable.class));
+        }
+        /*
+         * Finally store the result. The parsing pattern must be first because the call to
+         * Pattern.compile(regex) may thrown PatternSyntaxException. In such case, we want
+         * it to happen before we modified anything else.
+         */
+        if (regex != null) {
+            parsePattern = Pattern.compile(regex);
+            isParsePatternDefined = true;
+        } else {
+            parsePattern = null;
+            isParsePatternDefined = false;
+        }
+        omitTrailingNulls = trim;
+        separatorPrefix   = prefix;
+        separatorSuffix   = buffer.toString();
+        columnSeparator   = pattern.charAt(separatorIndex);
     }
 
     /**
@@ -356,7 +493,8 @@ public class TreeTableFormat extends Com
      *   <li>Each node shall be represented by a single line made of two parts, in that order:
      *     <ol>
      *       <li>white spaces and tree drawing characters ({@code '│'}, {@code '├'}, {@code '└'} or {@code '─'});</li>
-     *       <li>string representations of node values, separated by the {@linkplain #getColumnSeparator() colunm separator}.</li>
+     *       <li>string representations of node values, separated by the
+     *           {@linkplain #getColumnSeparatorPattern() colunm separator}.</li>
      *     </ol>
      *   </li>
      *   <li>The number of spaces and drawing characters before the node values determines the node
@@ -373,27 +511,38 @@ public class TreeTableFormat extends Com
      */
     @Override
     public TreeTable parse(final CharSequence text, final ParsePosition pos) throws ParseException {
-        final char separator    = columnSeparator.charAt(columnSeparator.length() - 1);
+        if (parsePattern == null) {
+            parsePattern = Pattern.compile(
+                    Pattern.quote(separatorPrefix)
+                  + Pattern.quote(String.valueOf(columnSeparator)) + '*'
+                  + Pattern.quote(separatorSuffix));
+        }
+        final Matcher matcher = parsePattern.matcher(text);
         final int length        = text.length();
         int indexOfLineStart    = pos.getIndex();
         int indentationLevel    = 0;                // Current index in the 'indentations' array.
         int[] indentations      = new int[16];      // Number of spaces (ignoring drawing characters) for each level.
         TreeTable.Node lastNode = null;             // Last parsed node, having 'indentation[level]' characters before its content.
         TreeTable.Node root     = null;             // First node found while parsing.
-        final DefaultTreeTable table = new DefaultTreeTable(columnIndices != null ? columnIndices : ColumnConstant.NAME_MAP);
+        final DefaultTreeTable table = new DefaultTreeTable(columnIndices != null ? columnIndices : TableColumn.NAME_MAP);
         final TableColumn<?>[] columns = DefaultTreeTable.getColumns(table.columnIndices);
         final Format[] formats = getFormats(columns, true);
         do {
             final int startNextLine = CharSequences.indexOfLineStart(text, 1, indexOfLineStart);
-            int endPosition = startNextLine;
-            while (endPosition > indexOfLineStart) {
-                final int c = text.charAt(endPosition-1);
+            int endOfLine = startNextLine;
+            while (endOfLine > indexOfLineStart) {
+                final int c = text.charAt(endOfLine-1);
                 if (c != '\r' && c != '\n') break;
-                endPosition--; // Skip trailing '\r' and '\n'.
+                endOfLine--; // Skip trailing '\r' and '\n'.
             }
+            /*
+             * Skip leading spaces using Character.isSpaceChar(…) instead than isWhitespace(…)
+             * because we need to skip non-breaking spaces as well as ordinary space. We don't
+             * need to consider line feeds since they were handled by the lines just above.
+             */
             boolean hasChar = false;
             int i; // The indentation of current line.
-            for (i=indexOfLineStart; i<endPosition;) {
+            for (i=indexOfLineStart; i<endOfLine;) {
                 final int c = Character.codePointAt(text, i);
                 if (!Character.isSpaceChar(c)) {
                     hasChar = true;
@@ -412,12 +561,7 @@ public class TreeTableFormat extends Com
              * user-spaces to interfer with the calculation of indentation.
              */
             int indexOfValue = i;
-            while (i > indexOfLineStart) {
-                final int c = Character.codePointBefore(text, i);
-                if (!Character.isSpaceChar(c)) break;
-                i -= Character.charCount(c);
-            }
-            i -= indexOfLineStart;
+            i = CharSequences.skipTrailingWhitespaces(text, indexOfLineStart, i) - indexOfLineStart;
             /*
              * Found the first character which is not part of the indentation. Create a new root
              * (without parent for now) and parse the values for each column. Columns with empty
@@ -425,24 +569,19 @@ public class TreeTableFormat extends Com
              */
             final TreeTable.Node node = new DefaultTreeTable.Node(table);
             try {
-parseColumns:   for (int ci=0; ci<columns.length; ci++) {
-                    int endOfColumn = CharSequences.indexOf(text, columnSeparator, indexOfValue, endPosition);
-                    if (endOfColumn < 0) {
-                        endOfColumn = endPosition;
-                    }
-                    for (int endOfValue = endOfColumn; endOfValue > indexOfValue;) {
-                        final int c = Character.codePointBefore(text, endOfValue);
-                        if (!Character.isSpaceChar(c)) {
-                            parseValue(node, columns[ci], formats[ci], text.subSequence(indexOfValue, endOfValue).toString());
-                            break;
-                        }
-                        endOfValue -= Character.charCount(c);
-                    }
-                    indexOfValue = endOfColumn;
-                    do if (++indexOfValue >= endPosition) {
-                        break parseColumns;
+                matcher.region(indexOfValue, endOfLine);
+                for (int ci=0; ci<columns.length; ci++) {
+                    final boolean found = matcher.find();
+                    int endOfColumn = found ? matcher.start() : endOfLine;
+                    indexOfValue   = CharSequences.skipLeadingWhitespaces (text, indexOfValue, endOfColumn);
+                    int endOfValue = CharSequences.skipTrailingWhitespaces(text, indexOfValue, endOfColumn);
+                    if (endOfValue > indexOfValue) {
+                        parseValue(node, columns[ci], formats[ci], text.subSequence(indexOfValue, endOfValue).toString());
                     }
-                    while (text.charAt(indexOfValue) == separator);
+                    if (!found) break;
+                    // The end of this column will be the beginning of the next column,
+                    // after skipping the last character of the column separator.
+                    indexOfValue = matcher.end();
                 }
             } catch (ParseException e) {
                 pos.setErrorIndex(indexOfValue);
@@ -584,7 +723,7 @@ parseColumns:   for (int ci=0; ci<column
          * For each indentation level, {@code true} if the previous levels are writing the last node.
          * This array will growth as needed.
          */
-        private boolean[] last;
+        private boolean[] isLast;
 
         /**
          * The columns to write.
@@ -597,9 +736,9 @@ parseColumns:   for (int ci=0; ci<column
         private final Format[] formats;
 
         /**
-         * The column separator character to repeat until the columns are aligned.
+         * The node values to format.
          */
-        private final char toRepeat;
+        private final Object[] values;
 
         /**
          * Creates a new instance which will write in the given appendable.
@@ -608,8 +747,8 @@ parseColumns:   for (int ci=0; ci<column
             super(columns.length >= 2 ? new TableFormatter(out, "") : out);
             this.columns  = columns;
             this.formats  = getFormats(columns, false);
-            this.last     = new boolean[8];
-            this.toRepeat = columnSeparator.charAt(columnSeparator.length() - 1);
+            this.values   = new Object[columns.length];
+            this.isLast   = new boolean[8];
             setTabulationExpanded(true);
             setLineSeparator(" ¶ ");
         }
@@ -653,24 +792,34 @@ parseColumns:   for (int ci=0; ci<column
          */
         final void format(final TreeTable.Node node, final int level) throws IOException {
             for (int i=0; i<level; i++) {
-                out.append(getTreeSymbols(i != level-1, last[i]));
+                out.append(getTreeSymbols(i != level-1, isLast[i]));
             }
+            int n = 0;
             for (int i=0; i<columns.length; i++) {
+                if ((values[i] = node.getValue(columns[i])) != null) {
+                    n = i;
+                }
+            }
+            if (!omitTrailingNulls) {
+                n = values.length - 1;
+            }
+            for (int i=0; i<=n; i++) {
                 if (i != 0) {
                     // We have a TableFormatter instance if and only if there is 2 or more columns.
-                    ((TableFormatter) out.append(columnSeparator)).nextColumn(toRepeat);
+                    ((TableFormatter) out.append(separatorPrefix)).nextColumn(columnSeparator);
+                    out.append(separatorSuffix);
                 }
-                formatValue(formats[i], node.getValue(columns[i]));
+                formatValue(formats[i], values[i]);
                 clear();
             }
             out.append(lineSeparator);
-            if (level >= last.length) {
-                last = Arrays.copyOf(last, level*2);
+            if (level >= isLast.length) {
+                isLast = Arrays.copyOf(isLast, level*2);
             }
             final List<? extends TreeTable.Node> children = node.getChildren();
             final int count = children.size();
             for (int i=0; i<count; i++) {
-                last[level] = (i == count-1);
+                isLast[level] = (i == count-1);
                 format(children.get(i), level+1);
             }
         }

Modified: sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java?rev=1415754&r1=1415753&r2=1415754&view=diff
==============================================================================
--- sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java (original)
+++ sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java Fri Nov 30 17:29:04 2012
@@ -41,11 +41,34 @@ public final class Errors extends Indexe
      * @version 0.3
      * @module
      */
-    public static final class Keys {
+    public static final class Keys extends KeyConstants {
+        /**
+         * The unique instance of key constants handler.
+         */
+        static final Keys INSTANCE = new Keys();
+
+        /**
+         * For {@link #INSTANCE} creation only.
+         */
         private Keys() {
         }
 
         /**
+         * Can not compute the derivative.
+         */
+        public static final int CanNotComputeDerivative = 44;
+
+        /**
+         * Can not clone an object of type ‘{0}’.
+         */
+        public static final int CloneNotSupported_1 = 42;
+
+        /**
+         * Thread “{0}” is dead.
+         */
+        public static final int DeadThread_1 = 43;
+
+        /**
          * Value “{0}” is duplicated.
          */
         public static final int DuplicatedValue_1 = 38;
@@ -127,6 +150,11 @@ public final class Errors extends Indexe
         public static final int IndexOutOfBounds_1 = 4;
 
         /**
+         * Argument ‘{0}’ can not take an infinite value.
+         */
+        public static final int InfiniteArgumentValue_1 = 45;
+
+        /**
          * A different value is already associated to the “{0}” key.
          */
         public static final int KeyCollision_1 = 19;
@@ -267,12 +295,11 @@ public final class Errors extends Indexe
     }
 
     /**
-     * Returns the {@code Keys} class.
+     * Returns the handle for the {@code Keys} constants.
      */
     @Override
-    final Class<?> getKeysClass() throws ClassNotFoundException {
-        assert super.getKeysClass() == Keys.class;
-        return Keys.class;
+    final KeyConstants getKeyConstants() {
+        return Keys.INSTANCE;
     }
 
     /**
@@ -375,9 +402,10 @@ public final class Errors extends Indexe
     private static final class International extends ResourceInternationalString {
         private static final long serialVersionUID = -229348959712294902L;
 
-        International(int key)              {super(key);}
-        International(int key, Object args) {super(key, args);}
-        @Override IndexedResourceBundle getBundle(Locale locale) {
+        International(int key)                   {super(key);}
+        International(int key, Object args)      {super(key, args);}
+        @Override KeyConstants getKeyConstants() {return Keys.INSTANCE;}
+        @Override IndexedResourceBundle getBundle(final Locale locale) {
             return getResources(locale);
         }
     }

Modified: sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties?rev=1415754&r1=1415753&r2=1415754&view=diff
==============================================================================
--- sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties (original)
+++ sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties Fri Nov 30 17:29:04 2012
@@ -14,6 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+CanNotComputeDerivative         = Can not compute the derivative.
+CloneNotSupported_1             = Can not clone an object of type \u2018{0}\u2019.
+DeadThread_1                    = Thread \u201c{0}\u201d is dead.
 DuplicatedValue_1               = Value \u201c{0}\u201d is duplicated.
 ElementAlreadyPresent_1         = Element \u201c{0}\u201d is already present.
 EmptyArgument_1                 = Argument \u2018{0}\u2019 shall not be empty.
@@ -30,6 +33,7 @@ IllegalRange_2                  = Range 
 InconsistentAttribute_2         = Value \u201c{1}\u201d of attribute \u2018{0}\u2019 is inconsistent with other attributes.
 InconsistentTableColumns        = Inconsistent table columns.
 IndexOutOfBounds_1              = Index {0} is out of bounds.
+InfiniteArgumentValue_1         = Argument \u2018{0}\u2019 can not take an infinite value.
 KeyCollision_1                  = A different value is already associated to the \u201c{0}\u201d key.
 MandatoryAttribute_2            = Attribute \u201c{0}\u201d is mandatory for an object of type \u2018{1}\u2019.
 NegativeArgument_2              = Argument \u2018{0}\u2019 shall not be negative. The given value was {1}.

Modified: sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties?rev=1415754&r1=1415753&r2=1415754&view=diff
==============================================================================
--- sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties (original)
+++ sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties Fri Nov 30 17:29:04 2012
@@ -14,6 +14,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
+CanNotComputeDerivative         = La d\u00e9riv\u00e9 ne peut pas \u00eatre calcul\u00e9e.
+CloneNotSupported_1             = Un objet de type \u2018{0}\u2019 ne peut pas \u00eatre clon\u00e9.
+DeadThread_1                    = La t\u00e2che \u201c{0}\u201d est morte.
 DuplicatedValue_1               = La valeur \u201c{0}\u201d est dupliqu\u00e9e.
 ElementAlreadyPresent_1         = L\u2019\u00e9lement \u201c{0}\u201d est d\u00e9j\u00e0 pr\u00e9sent.
 EmptyArgument_1                 = L\u2019argument \u2018{0}\u2019 ne doit pas \u00eatre vide.
@@ -30,6 +33,7 @@ IllegalRange_2                  = La pla
 InconsistentAttribute_2         = La valeur \u201c{1}\u201d de l\u2019attribut \u2018{0}\u2019 n\u2019est pas coh\u00e9rente avec celles des autres attributs.
 InconsistentTableColumns        = Les colonnes des tables ne sont pas coh\u00e9rentes.
 IndexOutOfBounds_1              = L\u2019index {0} est en dehors des limites permises.
+InfiniteArgumentValue_1         = L\u2019argument \u2018{0}\u2019 ne peut pas prendre une valeur infinie.
 KeyCollision_1                  = Une valeur diff\u00e9rente est d\u00e9j\u00e0 associ\u00e9e \u00e0 la cl\u00e9 \u201c{0}\u201d.
 MandatoryAttribute_2            = L\u2019attribut \u201c{0}\u201d est obligatoire pour un objet de type \u2018{1}\u2019.
 NegativeArgument_2              = L\u2019argument \u2018{0}\u2019 ne doit pas \u00eatre n\u00e9gatif. La valeur donn\u00e9e \u00e9tait {1}.

Modified: sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java?rev=1415754&r1=1415753&r2=1415754&view=diff
==============================================================================
--- sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java (original)
+++ sis/branches/JDK6/sis-utility/src/main/java/org/apache/sis/util/resources/IndexedResourceBundle.java Fri Nov 30 17:29:04 2012
@@ -22,7 +22,6 @@ import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.text.MessageFormat;
-import java.util.Arrays;
 import java.util.Enumeration;
 import java.util.Locale;
 import java.util.MissingResourceException;
@@ -30,8 +29,6 @@ import java.util.NoSuchElementException;
 import java.util.ResourceBundle;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
 import net.jcip.annotations.ThreadSafe;
 
 import org.opengis.util.InternationalString;
@@ -42,8 +39,6 @@ import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.logging.Logging;
 
-import static org.apache.sis.util.Arrays.resize;
-
 // Related to JDK7
 import org.apache.sis.internal.util.JDK7;
 
@@ -81,15 +76,6 @@ public class IndexedResourceBundle exten
     private final String filename;
 
     /**
-     * The key names. This is usually not needed, but may be created from the {@code Keys}
-     * inner class in some occasions.
-     *
-     * @see #getKeyNames()
-     * @see #getKeyName(int)
-     */
-    private transient String[] keys;
-
-    /**
      * The array of resources. Keys are an array index. For example, the value for key "14" is
      * {@code values[14]}. This array will be loaded only when first needed. We should not load
      * it at construction time, because some {@code ResourceBundle} objects will never ask for
@@ -175,54 +161,22 @@ public class IndexedResourceBundle exten
     }
 
     /**
-     * Returns the inner {@code Keys} class which declare the key constants.
+     * Returns a handler for the constants declared in the inner {@code Keys} class.
      * Subclasses defined in the {@code org.apache.sis.util.resources} package
      * override this method for efficiency. However the default implementation
      * should work for other cases (we don't want to expose too much internal API).
      *
-     * @return The inner {@code Keys} class.
-     * @throws ClassNotFoundException If the inner class has not been found.
+     * @return A handler for the constants declared in the inner {@code Keys} class.
      */
-    Class<?> getKeysClass() throws ClassNotFoundException {
+    KeyConstants getKeyConstants() {
+        Class<?> keysClass = KeyConstants.class;
         for (final Class<?> inner : getClass().getClasses()) {
             if ("Keys".equals(inner.getSimpleName())) {
-                return inner;
-            }
-        }
-        throw new ClassNotFoundException();
-    }
-
-    /**
-     * Returns the internal array of key names. <strong>Do not modify the returned array.</strong>
-     * This method should usually not be invoked, in order to avoid loading the inner Keys class.
-     * The keys names are used only in rare situation, like {@link #list(Writer)} or in log records.
-     */
-    private synchronized String[] getKeyNames() {
-        if (keys == null) {
-            String[] names;
-            int length = 0;
-            try {
-                final Field[] fields = getKeysClass().getFields();
-                names = new String[fields.length];
-                for (final Field field : fields) {
-                    if (Modifier.isStatic(field.getModifiers()) && field.getType() == Integer.TYPE) {
-                        final int index = (Integer) field.get(null);
-                        if (index >= length) {
-                            length = index + 1;
-                            if (length > names.length) {
-                                // Usually don't happen, except for incomplete bundles.
-                                names = Arrays.copyOf(names, length*2);
-                            }
-                        }
-                        names[index] = field.getName();
-                    }
-                }
-            } catch (Exception e) {
-                names = CharSequences.EMPTY_ARRAY;
+                keysClass = inner;
+                break;
             }
-            keys = resize(names, length);
         }
-        return keys;
+        return new KeyConstants(keysClass);
     }
 
     /**
@@ -232,7 +186,7 @@ public class IndexedResourceBundle exten
      */
     @Override
     public final Enumeration<String> getKeys() {
-        return new KeyEnum(getKeyNames());
+        return new KeyEnum(getKeyConstants().getKeyNames());
     }
 
     /**
@@ -272,22 +226,6 @@ public class IndexedResourceBundle exten
     }
 
     /**
-     * Returns the name of the key at the given index. If there is no name at that given
-     * index, format the index as a decimal number. Those decimal numbers are parsed by
-     * our {@link #handleGetObject(String)} implementation.
-     */
-    private String getKeyNameAt(final int index) {
-        final String[] keys = getKeyNames();
-        if (index < keys.length) {
-            final String key = keys[index];
-            if (key != null) {
-                return key;
-            }
-        }
-        return String.valueOf(index);
-    }
-
-    /**
      * Lists resources to the specified stream. If a resource has more than one line, only
      * the first line will be written. This method is used mostly for debugging purposes.
      *
@@ -297,7 +235,7 @@ public class IndexedResourceBundle exten
     @Debug
     public final void list(final Appendable out) throws IOException {
         int keyLength = 0;
-        final String[] keys = getKeyNames();
+        final String[] keys = getKeyConstants().getKeyNames();
         for (final String key : keys) {
             if (key != null) {
                 keyLength = Math.max(keyLength, key.length());
@@ -429,7 +367,7 @@ public class IndexedResourceBundle exten
              * LogRecords, for easier debugging if the message has not been properly formatted.
              */
             try {
-                keyID = (Integer) getKeysClass().getField(key).get(null);
+                keyID = getKeyConstants().getKeyValue(key);
             } catch (Exception e) {
                 Logging.recoverableException(getClass(), "handleGetObject", e);
                 return null; // This is okay as of 'handleGetObject' contract.
@@ -668,7 +606,7 @@ public class IndexedResourceBundle exten
      * @return The log record.
      */
     public final LogRecord getLogRecord(final Level level, final int key) {
-        final LogRecord record = new LogRecord(level, getKeyNameAt(key));
+        final LogRecord record = new LogRecord(level, getKeyConstants().getKeyName(key));
         record.setResourceBundleName(getClass().getName());
         record.setResourceBundle(this);
         return record;



Mime
View raw message