sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From tlpin...@apache.org
Subject svn commit: r1517321 [12/16] - in /sis/branches/Shapefile: ./ application/ application/sis-console/ application/sis-console/src/main/artifact/ application/sis-console/src/main/java/org/apache/sis/console/ application/sis-console/src/main/resources/org/...
Date Sun, 25 Aug 2013 15:49:59 GMT
Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -59,45 +59,64 @@ import org.apache.sis.internal.jdk7.Obje
  *   <tr><td>{@code s}</td><td>The fractional part of seconds</td></tr>
  *   <tr><td>{@code #}</td><td>Fraction digits shown only if non-zero</td></tr>
  *   <tr><td>{@code .}</td><td>The decimal separator</td></tr>
+ *   <tr><td>{@code ?}</td><td>Omit the preceding field if zero</td></tr>
  * </table>
  *
  * Upper-case letters {@code D}, {@code M} and {@code S} stand for the integer parts of degrees,
- * minutes and seconds respectively. They shall appear in this order. For example {@code M'D} is
- * illegal because "M" and "S" are in reverse order; {@code D°S} is illegal too because "M" is
- * missing between "D" and "S".
+ * minutes and seconds respectively. If present, they shall appear in that order.
  *
- * <p>Lower-case letters {@code d}, {@code m} and {@code s} stand for fractional parts of degrees,
- * minutes and seconds respectively. Only one of those may appears in a pattern, and it must be
- * the last special symbol. For example {@code D.dd°MM'} is illegal because "d" is followed by
- * "M"; {@code D.mm} is illegal because "m" is not the fractional part of "D".</p>
+ * {@example "<code>M′D</code>" is illegal because "<code>M</code>" and "<code>S</code>" are in reverse order.
+ *           "<code>D°S</code>" is also illegal because "<code>M</code>" is missing between "<code>D</code>" and
+ *           "<code>S</code>".}
  *
- * <p>The number of occurrence of {@code D}, {@code M}, {@code S} and their lower-case counterpart
- * is the number of digits to format. For example, {@code DD.ddd} will format angles with two digits
- * for the integer part and three digits for the fractional part (e.g. 4.4578 will be formatted as
- * "04.458").</p>
+ * Lower-case letters {@code d}, {@code m} and {@code s} stand for fractional parts of degrees, minutes and
+ * seconds respectively. Only one of those can appear in a pattern. If present, they must be in the last field.
  *
- * <p>Separator characters like {@code °}, {@code ′} and {@code ″} are inserted "as-is" in the
- * formatted string, except the decimal separator dot ({@code .}) which is replaced by the
- * local-dependent decimal separator. Separator characters may be completely omitted;
- * {@code AngleFormat} will still differentiate degrees, minutes and seconds fields according
- * the pattern. For example, "{@code 0480439}" with the pattern {@code DDDMMmm} will be parsed
- * as 48°04.39'.</p>
+ * {@example "<code>D.dd°MM′</code>" is illegal because "<code>d</code>" is followed by "<code>M</code>".
+ *           "<code>D.mm</code>" is also illegal because "<code>m</code>" is not the fractional part of
+ *           "<code>D</code>".}
  *
- * <p>The following table gives some pattern examples:</p>
+ * The number of occurrences of {@code D}, {@code M}, {@code S} and their lower-case counterpart is the number
+ * of digits to format.
  *
+ * {@example "<code>DD.ddd</code>" will format angles with two digits for the integer part and three digits
+ *           for the fractional part (e.g. <code>4.4578</code> will be formatted as <code>"04.458"</code>).}
+ *
+ * Separator characters like {@code °}, {@code ′} and {@code ″} are inserted "as-is" in the formatted string,
+ * except the decimal separator dot ({@code .}) which is replaced by the local-dependent decimal separator.
+ * Separator characters may be completely omitted; {@code AngleFormat} will still differentiate degrees,
+ * minutes and seconds fields according the pattern.
+ *
+ * {@example "<code>0480439</code>" with the "<code>DDDMMmm</code>" pattern will be parsed as 48°04.39′.}
+ *
+ * The {@code ?} modifier specifies that the preceding field can be omitted if its value is zero.
+ * Any field can be omitted for {@link Angle} object, but only trailing fields are omitted for
+ * {@li{@link Longitude} and {@link Latitude}.
+ *
+ * {@example "<code>DD°MM′?SS″?</code>" will format an angle of 12.01° as <code>12°36″</code>,
+ *           but a longitude of 12.01°N as <code>12°00′36″N</code> (not <code>12°36″N</code>).}
+ *
+ * The above special case exists because some kind of angles are expected to be very small (e.g. rotation angles in
+ * {@linkplain org.apache.sis.referencing.datum.BursaWolfParameters Bursa-Wolf parameters} are given in arc-seconds),
+ * while longitude and latitude values are usually distributed over their full ±180° or ±90° range. Since longitude
+ * or latitude values without the degrees field are unusual, omitting that field is likely to increase the
+ * risk of confusion in those cases.
+ *
+ * {@section Examples}
  * <table class="sis">
- *   <tr><th>Pattern           </th>  <th>Example   </th></tr>
- *   <tr><td>{@code DD°MM′SS″ }</td>  <td>48°30′00″ </td></tr>
- *   <tr><td>{@code DD°MM′    }</td>  <td>48°30′    </td></tr>
- *   <tr><td>{@code DD.ddd    }</td>  <td>48.500    </td></tr>
- *   <tr><td>{@code DD.###    }</td>  <td>48.5      </td></tr>
- *   <tr><td>{@code DDMM      }</td>  <td>4830      </td></tr>
- *   <tr><td>{@code DDMMSS    }</td>  <td>483000    </td></tr>
+ *   <tr><th>Pattern               </th>  <th>48.5      </th> <th>-12.53125    </th></tr>
+ *   <tr><td>{@code DD°MM′SS.#″}   </td>  <td>48°30′00″ </td> <td>-12°31′52.5″ </td></tr>
+ *   <tr><td>{@code DD°MM′}        </td>  <td>48°30′    </td> <td>-12°32′      </td></tr>
+ *   <tr><td>{@code DD.ddd}        </td>  <td>48.500    </td> <td>-12.531      </td></tr>
+ *   <tr><td>{@code DD.###}        </td>  <td>48.5      </td> <td>-12.531      </td></tr>
+ *   <tr><td>{@code DDMM}          </td>  <td>4830      </td> <td>-1232        </td></tr>
+ *   <tr><td>{@code DDMMSSs}       </td>  <td>4830000   </td> <td>-1231525     </td></tr>
+ *   <tr><td>{@code DD°MM′?SS.s″?} </td>  <td>48°30′    </td> <td>-12°31′52.5″ </td></tr>
  * </table>
  *
  * @author  Martin Desruisseaux (MPO, IRD, Geomatys)
  * @since   0.3 (derived from geotk-1.0)
- * @version 0.3
+ * @version 0.4
  * @module
  *
  * @see Angle
@@ -165,10 +184,15 @@ public class AngleFormat extends Format 
     static final int HEMISPHERE_FIELD = 4;
 
     /**
+     * Index for the {@link #SYMBOLS} character which stands for optional field.
+     */
+    private static final int OPTIONAL_FIELD = 4;
+
+    /**
      * Symbols for degrees (0), minutes (1), seconds (2) and optional fraction digits (3).
      * The index of each symbol shall be equal to the corresponding {@code *_FIELD} constant.
      */
-    private static final char[] SYMBOLS = {'D', 'M', 'S', '#'};
+    private static final int[] SYMBOLS = {'D', 'M', 'S', '#', '?'};
 
     /**
      * Defines constants that are used as attribute keys in the iterator returned from
@@ -254,6 +278,13 @@ public class AngleFormat extends Format 
                  maximumTotalWidth;
 
     /**
+     * A bitmask of optional fields. Optional fields are formatted only if their value is different than zero.
+     * The bit position is given by a {@code *_FIELD} constant, and the actual bitmask is computed by
+     * {@code 1 << *_FIELD}. A value of zero means that no field is optional.
+     */
+    private byte optionalFields;
+
+    /**
      * Characters to insert before the text to format, and after each field.
      * A {@code null} value means that there is nothing to insert.
      */
@@ -282,6 +313,16 @@ public class AngleFormat extends Format 
     private boolean useDecimalSeparator;
 
     /**
+     * If {@code true}, {@link #optionalFields} never apply to fields to leading fields.
+     * If the minutes field is declared optional but the degrees and seconds are formatted,
+     * then minutes will be formatted too un order to reduce the risk of confusion
+     *
+     * {@example Value 12.01 is formatted as <code>12°00′36″</code> if <code>true</code>
+     *           and as <code>12°36″</code> if <code>false</code>.}
+     */
+    private transient boolean showLeadingFields;
+
+    /**
      * Format to use for writing numbers (degrees, minutes or seconds) when formatting an angle.
      * The pattern given to this {@code DecimalFormat} shall NOT accept exponential notation,
      * because "E" of "Exponent" would be confused with "E" of "East".
@@ -360,14 +401,15 @@ public class AngleFormat extends Format 
     public AngleFormat(final Locale locale) {
         ArgumentChecks.ensureNonNull("locale", locale);
         this.locale = locale;
-        degreesFieldWidth     = 1;
-        minutesFieldWidth     = 2;
-        secondsFieldWidth     = 2;
-        fractionFieldWidth    = 16;  // Number of digits for accurate representation of 1″ ULP.
-        degreesSuffix         = "°";
-        minutesSuffix         = "′";
-        secondsSuffix         = "″";
-        useDecimalSeparator   = true;
+        degreesFieldWidth   = 1;
+        minutesFieldWidth   = 2;
+        secondsFieldWidth   = 2;
+        fractionFieldWidth  = 16;  // Number of digits for accurate representation of 1″ ULP.
+        optionalFields      = (1 << DEGREES_FIELD) | (1 << MINUTES_FIELD) | (1 << SECONDS_FIELD);
+        degreesSuffix       = "°";
+        minutesSuffix       = "′";
+        secondsSuffix       = "″";
+        useDecimalSeparator = true;
     }
 
     /**
@@ -390,6 +432,7 @@ public class AngleFormat extends Format 
      * @throws IllegalArgumentException If the specified pattern is illegal.
      */
     public AngleFormat(final String pattern, final Locale locale) throws IllegalArgumentException {
+        ArgumentChecks.ensureNonEmpty("pattern", pattern);
         ArgumentChecks.ensureNonNull("locale", locale);
         this.locale = locale;
         applyPattern(pattern, SYMBOLS, '.');
@@ -406,137 +449,182 @@ public class AngleFormat extends Format 
      * @see #setMaximumFractionDigits(int)
      */
     public void applyPattern(final String pattern) throws IllegalArgumentException {
-        applyPattern(pattern, SYMBOLS, '.');
-    }
-
-    /**
-     * Actual implementation of {@link #applyPattern(String)},
-     * as a private method for use by the constructor.
-     *
-     * @param symbols An array of 3 characters containing the reserved symbols as upper-case letters.
-     *        This is always the {@link #SYMBOLS} array, unless we apply localized patterns.
-     * @param decimalSeparator The code point which represent decimal separator in the pattern.
-     */
-    @SuppressWarnings("fallthrough")
-    private void applyPattern(final String pattern, final char[] symbols, final int decimalSeparator) {
         ArgumentChecks.ensureNonEmpty("pattern", pattern);
-        degreesFieldWidth     = 1;
+        degreesFieldWidth     = 0;
         minutesFieldWidth     = 0;
         secondsFieldWidth     = 0;
         fractionFieldWidth    = 0;
         minimumFractionDigits = 0;
         maximumTotalWidth     = 0;
+        optionalFields        = 0;
         prefix                = null;
         degreesSuffix         = null;
         minutesSuffix         = null;
         secondsSuffix         = null;
+        useDecimalSeparator   = false;
+        applyPattern(pattern, SYMBOLS, '.');
+    }
+
+    /**
+     * Actual implementation of {@link #applyPattern(String)}, as a private method for use by the constructor.
+     * All fields related to the pattern shall be set to 0 or null before this method call.
+     *
+     * @param symbols An array of code points containing the reserved symbols as upper-case letters.
+     *        This is always the {@link #SYMBOLS} array, unless we apply localized patterns.
+     * @param decimalSeparator The code point which represent decimal separator in the pattern.
+     */
+    @SuppressWarnings("fallthrough")
+    private void applyPattern(final String pattern, final int[] symbols, final int decimalSeparator) {
+        degreesFieldWidth     = 1;
         useDecimalSeparator   = true;
         int expectedField     = PREFIX_FIELD;
         int endPreviousField  = 0;
         boolean parseFinished = false;
         final int length = pattern.length();
-scan:   for (int i=0; i<length;) {
+        for (int i=0; i<length;) {
             /*
              * Examine the first characters in the pattern, skipping the non-reserved ones
              * ("D", "M", "S", "d", "m", "s", "#"). Non-reserved characters will be stored
-             * as suffix later.
+             * as prefix or suffix later.
              */
-            int c          = pattern.codePointAt(i);
-            int charCount  = Character.charCount(c);
-            int upperCaseC = Character.toUpperCase(c);
-            for (int field=DEGREES_FIELD; field<=FRACTION_FIELD; field++) {
-                if (upperCaseC != symbols[field]) {
-                    continue;
-                }
+            int c           = pattern.codePointAt(i);
+            int charCount   = Character.charCount(c);
+            int upperCaseC  = Character.toUpperCase(c);
+            final int field = fieldForSymbol(symbols, upperCaseC);
+            if (field < 0) { // If not a reserved character, continue the search.
+                i += charCount;
+                continue;
+            }
+            /*
+             * A reserved character has been found.  Ensure that it appears in a legal location.
+             * For example "MM.mm" is illegal because there is no 'D' before 'M', and "DD.mm" is
+             * illegal because the integer part is not 'M'. The legal location is 'expectedField'.
+             */
+            final boolean isIntegerField = (c == upperCaseC) && (field != FRACTION_FIELD);
+            if (isIntegerField) {
+                expectedField++;
+            }
+            if (parseFinished || (field != expectedField && field != FRACTION_FIELD)) {
+                throw illegalPattern(pattern);
+            }
+            if (isIntegerField) {
                 /*
-                 * A reserved character has been found.  Ensure that it appears in a legal
-                 * location. For example "MM.mm" is illegal because there is no 'D' before
-                 * 'M', and "DD.mm" is illegal because the integer part is not 'M'.
+                 * If the reserved letter is upper-case, then we found the integer part of a field.
+                 * Memorize the characters prior the reserved letter as the suffix of the previous field.
+                 * Then count the number of occurrences of that reserved letter. This number will be the
+                 * field width.
                  */
-                final boolean isIntegerField = (c == upperCaseC) && (field != FRACTION_FIELD);
-                if (isIntegerField) {
-                    expectedField++;
-                }
-                if (parseFinished || (field != expectedField && field != FRACTION_FIELD)) {
-                    throw new IllegalArgumentException(Errors.format(
-                            Errors.Keys.IllegalFormatPatternForClass_2, Angle.class, pattern));
-                }
-                if (isIntegerField) {
-                    /*
-                     * Memorize the characters prior the reserved letter as the suffix of
-                     * the previous field. Then count the number of occurrences of that
-                     * reserved letter. This number will be the field width.
-                     */
-                    final String previousSuffix = (i > endPreviousField) ? pattern.substring(endPreviousField, i) : null;
-                    int width = 1;
-                    while ((i += charCount) < length && pattern.codePointAt(i) == c) {
-                        width++;
-                    }
-                    final byte wb = toByte(width);
-                    switch (field) {
-                        case DEGREES_FIELD: prefix        = previousSuffix; degreesFieldWidth = wb; break;
-                        case MINUTES_FIELD: degreesSuffix = previousSuffix; minutesFieldWidth = wb; break;
-                        case SECONDS_FIELD: minutesSuffix = previousSuffix; secondsFieldWidth = wb; break;
-                        default: throw new AssertionError(field);
-                    }
-                } else {
-                    /*
-                     * If the reserved letter is lower-case or the symbol for optional fraction
-                     * digit, the part before that letter will be the decimal separator rather
-                     * than the suffix of previous field. The count the number of occurrences of
-                     * the lower-case letter; this will be the precision of the fraction part.
-                     */
-                    if (i == endPreviousField) {
-                        useDecimalSeparator = false;
-                    } else {
-                        final int b = pattern.codePointAt(endPreviousField);
-                        if (b != decimalSeparator || endPreviousField + Character.charCount(b) != i) {
-                            throw new IllegalArgumentException(Errors.format(
-                                    Errors.Keys.IllegalFormatPatternForClass_2, Angle.class, pattern));
-                        }
-                    }
-                    int width = 1;
-                    while ((i += charCount) < length) {
-                        final int fc = pattern.codePointAt(i);
-                        if (fc != c) {
-                            if (fc != symbols[FRACTION_FIELD]) break;
-                            // Switch the search from mandatory to optional digits.
-                            minimumFractionDigits = toByte(width);
-                            charCount = Character.charCount(c = fc);
+                String previousSuffix = null;
+                if (endPreviousField < i) {
+                    int endPreviousSuffix = i;
+                    if (pattern.codePointBefore(endPreviousSuffix) == symbols[OPTIONAL_FIELD]) {
+                        // If we find the '?' character, then the previous field is optional.
+                        if (--endPreviousSuffix == endPreviousField) {
+                            throw illegalPattern(pattern);
                         }
-                        width++;
+                        optionalFields |= (1 << (field - 1));
                     }
-                    fractionFieldWidth = toByte(width);
-                    if (c != symbols[FRACTION_FIELD]) {
-                        // The pattern contains only mandatory digits.
-                        minimumFractionDigits = fractionFieldWidth;
-                    } else if (!useDecimalSeparator) {
-                        // Variable number of digits not allowed if there is no decimal separator.
-                        throw new IllegalArgumentException(Errors.format(Errors.Keys.RequireDecimalSeparator));
-                    }
-                    parseFinished = true;
+                    previousSuffix = pattern.substring(endPreviousField, endPreviousSuffix);
+                }
+                int width = 1;
+                while ((i += charCount) < length && pattern.codePointAt(i) == c) {
+                    width++;
+                }
+                final byte wb = toByte(width);
+                switch (field) {
+                    case DEGREES_FIELD: prefix        = previousSuffix; degreesFieldWidth = wb; break;
+                    case MINUTES_FIELD: degreesSuffix = previousSuffix; minutesFieldWidth = wb; break;
+                    case SECONDS_FIELD: minutesSuffix = previousSuffix; secondsFieldWidth = wb; break;
+                    default: throw new AssertionError(field);
+                }
+            } else {
+                /*
+                 * If the reserved letter is lower-case or the symbol for optional fraction digit,
+                 * then the part before that letter will be the decimal separator rather than the
+                 * suffix of previous field. The number of occurrences of the lower-case letter will
+                 * be the precision of the fraction part.
+                 */
+                if (i == endPreviousField) {
+                    useDecimalSeparator = false;
+                } else {
+                    final int b = pattern.codePointAt(endPreviousField);
+                    if (b != decimalSeparator || endPreviousField + Character.charCount(b) != i) {
+                        throw illegalPattern(pattern);
+                    }
+                }
+                int width = 1;
+                while ((i += charCount) < length) {
+                    final int fc = pattern.codePointAt(i);
+                    if (fc != c) {
+                        if (fc != symbols[FRACTION_FIELD]) break;
+                        // Switch the search from mandatory to optional digits.
+                        minimumFractionDigits = toByte(width);
+                        charCount = Character.charCount(c = fc);
+                    }
+                    width++;
+                }
+                fractionFieldWidth = toByte(width);
+                if (c != symbols[FRACTION_FIELD]) {
+                    // The pattern contains only mandatory digits.
+                    minimumFractionDigits = fractionFieldWidth;
+                } else if (!useDecimalSeparator) {
+                    // Variable number of digits not allowed if there is no decimal separator.
+                    throw new IllegalArgumentException(Errors.format(Errors.Keys.RequireDecimalSeparator));
                 }
-                endPreviousField = i;
-                continue scan;
+                parseFinished = true;
             }
-            i += charCount;
+            endPreviousField = i;
         }
+        /*
+         * At this point, we finished parsing the pattern. We may have some trailing characters which have not
+         * been processed by the main loop. Those trailing characters will be the suffix of the last field.
+         */
         if (endPreviousField < length) {
-            final String suffix = pattern.substring(endPreviousField);
+            int endPreviousSuffix = length;
+            if (pattern.codePointBefore(endPreviousSuffix) == symbols[OPTIONAL_FIELD]) {
+                if (--endPreviousSuffix == endPreviousField) {
+                    throw illegalPattern(pattern);
+                }
+                optionalFields |= (1 << expectedField);
+            }
+            final String suffix = pattern.substring(endPreviousField, endPreviousSuffix);
             switch (expectedField) {
                 case DEGREES_FIELD: degreesSuffix = suffix; break;
                 case MINUTES_FIELD: minutesSuffix = suffix; break;
                 case SECONDS_FIELD: secondsSuffix = suffix; break;
                 default: {
                     // Happen if no symbol has been recognized in the pattern.
-                    throw new IllegalArgumentException(Errors.format(
-                            Errors.Keys.IllegalFormatPatternForClass_2, Angle.class, pattern));
+                    throw illegalPattern(pattern);
                 }
             }
         }
     }
 
     /**
+     * Returns the field index for the given upper case character, or -1 if none.
+     *
+     * @param  symbols An array of code points containing the reserved symbols as upper-case letters.
+     * @param  c The symbol to search, as an upper-case character (code point actually).
+     * @return The index of the given character, or -1 if not found.
+     */
+    private static int fieldForSymbol(final int[] symbols, final int c) {
+        for (int field=DEGREES_FIELD; field<=FRACTION_FIELD; field++) {
+            if (c == symbols[field]) {
+                return field;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Returns an exception for an illegal pattern.
+     */
+    private static IllegalArgumentException illegalPattern(final String pattern) {
+        return new IllegalArgumentException(Errors.format(
+                Errors.Keys.IllegalFormatPatternForClass_2, Angle.class, pattern));
+    }
+
+    /**
      * Returns the pattern used for parsing and formatting angles.
      * See class description for an explanation of how patterns work.
      *
@@ -550,14 +638,16 @@ scan:   for (int i=0; i<length;) {
     }
 
     /**
-     * Actual implementation of {@link #toPattern()}.
+     * Actual implementation of {@link #toPattern()} and {@code toLocalizedPattern()}
+     * (the later method may be provided in a future SIS version).
      *
-     * @param symbols An array of 3 characters containing the reserved symbols as upper-case letters.
+     * @param symbols An array of code points containing the reserved symbols as upper-case letters.
      *        This is always the {@link #SYMBOLS} array, unless we apply localized patterns.
      * @param decimalSeparator The code point which represent decimal separator in the pattern.
      */
-    private String toPattern(final char[] symbols, final int decimalSeparator) {
-        char symbol = 0;
+    private String toPattern(final int[] symbols, final int decimalSeparator) {
+        int symbol = 0;
+        boolean previousWasOptional = false;
         final StringBuilder buffer = new StringBuilder();
         for (int field=DEGREES_FIELD; field<=FRACTION_FIELD; field++) {
             final String previousSuffix;
@@ -587,26 +677,33 @@ scan:   for (int i=0; i<length;) {
                         if (width == optional) {
                             symbol = symbols[FRACTION_FIELD];
                         }
-                        buffer.append(symbol);
+                        buffer.appendCodePoint(symbol);
                     }
                     while (--width > 0);
                 }
-                if (previousSuffix != null) {
-                    buffer.append(previousSuffix);
-                }
-                break; // We are done.
+                /*
+                 * The code for writing the suffix is common to this "if" case (the fraction part of
+                 * the pattern) and the "normal" case below. So we write the suffix outside the "if"
+                 * block and will exit the main loop immediately after that.
+                 */
             }
-            /*
-             * This is the normal part of the loop, before the final fractional part handled
-             * in the above block. Write the suffix of the previous field, then the pattern
-             * for the integer part of degrees, minutes or second field.
-             */
             if (previousSuffix != null) {
                 buffer.append(previousSuffix);
             }
+            if (previousWasOptional) {
+                buffer.appendCodePoint(symbols[OPTIONAL_FIELD]);
+            }
+            if (width <= 0) {
+                break; // The "if" case above has been executed for writing the fractional part, so we are done.
+            }
+            /*
+             * This is the main part of the loop, before the final fractional part handled in the above "if" case.
+             * Write the pattern for the integer part of degrees, minutes or second field.
+             */
             symbol = symbols[field];
-            do buffer.append(symbol);
+            do buffer.appendCodePoint(symbol);
             while (--width > 0);
+            previousWasOptional = (optionalFields & (1 << field)) != 0;
         }
         return buffer.toString();
     }
@@ -809,32 +906,44 @@ scan:   for (int i=0; i<length;) {
             }
             return toAppendTo;
         }
-        double degrees = angle;
         /*
          * Computes the numerical values of minutes and seconds fields.
          * If those fiels are not written, then store NaN.
          */
+        double degrees = angle;
         double minutes = NaN;
         double seconds = NaN;
         if (minutesFieldWidth != 0 && !isNaN(angle)) {
             minutes = abs(degrees - (degrees = truncate(degrees))) * 60;
+            final double p = pow10(fractionFieldWidth);
             if (secondsFieldWidth != 0) {
                 seconds = (minutes - (minutes = truncate(minutes))) * 60;
-                /*
-                 * Correction for rounding errors.
-                 */
-                final double puissance = pow10(fractionFieldWidth);
-                seconds = rint(seconds * puissance) / puissance;
-                final double correction = truncate(seconds / 60);
-                seconds -= correction * 60;
-                minutes += correction;
+                seconds = rint(seconds * p) / p; // Correction for rounding errors.
+                if (seconds >= 60) { // We do not expect > 60 (only == 60), but let be safe.
+                    seconds = 0;
+                    minutes++;
+                }
             } else {
-                final double puissance = pow10(fractionFieldWidth);
-                minutes = rint(minutes * puissance) / puissance;
+                minutes = rint(minutes * p) / p; // Correction for rounding errors.
+            }
+            if (minutes >= 60) { // We do not expect > 60 (only == 60), but let be safe.
+                minutes = 0;
+                degrees += Math.signum(angle);
+            }
+            // Note: a previous version was doing a unconditional addition to the 'degrees' variable,
+            // in the form 'degrees += correction'. However -0.0 + 0 == +0.0, while we really need to
+            // preserve the sign of negative zero. See [SIS-120].
+        }
+        /*
+         * Avoid formatting values like 12.01°N as 12°36″N because of the risk of confusion.
+         * In such cases, force the formatting of minutes field as in 12°00′36″.
+         */
+        byte effectiveOptionalFields = optionalFields;
+        if (showLeadingFields) {
+            effectiveOptionalFields &= ~(1 << DEGREES_FIELD);
+            if (minutes == 0 && ((effectiveOptionalFields & (1 << SECONDS_FIELD)) == 0 || seconds != 0)) {
+                effectiveOptionalFields &= ~(1 << MINUTES_FIELD);
             }
-            final double correction = truncate(minutes / 60);
-            minutes -= correction * 60;
-            degrees += correction;
         }
         /*
          * At this point the 'degrees', 'minutes' and 'seconds' variables contain the final values
@@ -853,11 +962,10 @@ scan:   for (int i=0; i<length;) {
             }
         }
         /*
-         * Formats fields in a loop from DEGREES_FIELD to SECONDS_FIELD inclusive.
-         * The first part of the loop will configure the NumberFormat, but without
-         * writing anything yet (ignoring the prefix written before the loop).
+         * The following loop will format fields from DEGREES_FIELD to SECONDS_FIELD inclusive.
+         * The NumberFormat will be reconfigured at each iteration.
          */
-        int field = DEGREES_FIELD;
+        int field = PREFIX_FIELD;
         if (prefix != null) {
             toAppendTo.append(prefix);
         }
@@ -867,12 +975,26 @@ scan:   for (int i=0; i<length;) {
             int    width;
             double value;
             String suffix;
-            switch (field) {
+            switch (++field) {
                 case DEGREES_FIELD: value=degrees; width=degreesFieldWidth; suffix=degreesSuffix; hasMore=(minutesFieldWidth != 0); break;
                 case MINUTES_FIELD: value=minutes; width=minutesFieldWidth; suffix=minutesSuffix; hasMore=(secondsFieldWidth != 0); break;
                 case SECONDS_FIELD: value=seconds; width=secondsFieldWidth; suffix=secondsSuffix; hasMore=false; break;
                 default: throw new AssertionError(field);
             }
+            /*
+             * If the value is zero and the field is optional, propagate the sign to the next field
+             * and skip the whole field. Otherwise process to the formatting of current field.
+             */
+            if (value == 0 && (effectiveOptionalFields & (1 << field)) != 0) {
+                switch (field) {
+                    case DEGREES_FIELD: minutes = Math.copySign(minutes, degrees); break;
+                    case MINUTES_FIELD: seconds = Math.copySign(seconds, minutes); break;
+                }
+                continue;
+            }
+            /*
+             * Configure the NumberFormat for the number of digits to write, but do not write anything yet.
+             */
             if (hasMore) {
                 numberFormat.setMinimumIntegerDigits(width);
                 numberFormat.setMaximumFractionDigits(0);
@@ -883,9 +1005,10 @@ scan:   for (int i=0; i<length;) {
                      * If we are required to fit the formatted angle in some maximal total width
                      * (i.e. the user called the setMaximumWidth(int) method), compute the space
                      * available for fraction digits after we removed the space for the integer
-                     * digits, the decimal separator (this is the -1 below) and the suffix.
+                     * digits, the decimal separator (this is the +1 below) and the suffix.
                      */
-                    int available = maximumTotalWidth - toAppendTo.codePointCount(offset, toAppendTo.length()) - width - 1;
+                    int available = maximumTotalWidth - toAppendTo.codePointCount(offset, toAppendTo.length());
+                    available -= (width + 1); // Remove the amount of code points that we plan to write.
                     if (suffix != null) {
                         width -= suffix.length();
                     }
@@ -932,7 +1055,6 @@ scan:   for (int i=0; i<length;) {
                 pos.setBeginIndex(startPosition);
                 pos.setEndIndex(toAppendTo.length());
             }
-            field++;
         } while (hasMore);
         return toAppendTo;
     }
@@ -992,7 +1114,12 @@ scan:   for (int i=0; i<length;) {
     private StringBuffer format(final double angle, StringBuffer toAppendTo,
             final FieldPosition pos, final char positiveSuffix, final char negativeSuffix)
     {
-        toAppendTo = format(abs(angle), toAppendTo, pos);
+        try {
+            showLeadingFields = true;
+            toAppendTo = format(abs(angle), toAppendTo, pos);
+        } finally {
+            showLeadingFields = false;
+        }
         final int startPosition = toAppendTo.length();
         final char suffix = isNegative(angle) ? negativeSuffix : positiveSuffix;
         toAppendTo.append(suffix);
@@ -1633,9 +1760,9 @@ BigBoss:    switch (skipSuffix(source, p
      */
     @Override
     public int hashCode() {
-        return Objects.hash(degreesFieldWidth, minutesFieldWidth, secondsFieldWidth,
-                fractionFieldWidth, minimumFractionDigits, useDecimalSeparator, isFallbackAllowed,
-                locale, prefix, degreesSuffix, minutesSuffix, secondsSuffix) ^ (int) serialVersionUID;
+        return Objects.hash(degreesFieldWidth, minutesFieldWidth, secondsFieldWidth, fractionFieldWidth,
+                minimumFractionDigits, useDecimalSeparator, isFallbackAllowed, optionalFields, locale,
+                prefix, degreesSuffix, minutesSuffix, secondsSuffix) ^ (int) serialVersionUID;
     }
 
     /**
@@ -1657,14 +1784,14 @@ BigBoss:    switch (skipSuffix(source, p
                    minimumFractionDigits == cast.minimumFractionDigits &&
                    useDecimalSeparator   == cast.useDecimalSeparator   &&
                    isFallbackAllowed     == cast.isFallbackAllowed     &&
+                   optionalFields        == cast.optionalFields        &&
                    Objects.equals(locale,        cast.locale)          &&
                    Objects.equals(prefix,        cast.prefix)          &&
                    Objects.equals(degreesSuffix, cast.degreesSuffix)   &&
                    Objects.equals(minutesSuffix, cast.minutesSuffix)   &&
                    Objects.equals(secondsSuffix, cast.secondsSuffix);
-        } else {
-            return false;
         }
+        return false;
     }
 
     /**

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/setup/About.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/setup/About.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/setup/About.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/setup/About.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -402,7 +402,7 @@ pathTree:   for (int j=0; ; j++) {
                         directory.setValue(NAME, parenthesis(resources.getString(homeKey)));
                     }
                     CharSequence title = entry.getValue();
-                    if (title == null) {
+                    if (title == null || title.length() == 0) {
                         title = parenthesis(resources.getString(entry.getKey().isDirectory() ?
                                 Vocabulary.Keys.Directory : Vocabulary.Keys.Untitled).toLowerCase(locale));
                     }
@@ -488,6 +488,11 @@ pathTree:   for (int j=0; ; j++) {
                             if (title == null) {
                                 title = concatenate(attributes.getValue(Attributes.Name.SPECIFICATION_TITLE),
                                         attributes.getValue(Attributes.Name.SPECIFICATION_VERSION), false);
+                                if (title == null) {
+                                    // We really need a non-null value in order to protect this code
+                                    // against infinite recursivity.
+                                    title = "";
+                                }
                             }
                             entry.setValue(title);
                             files = classpath(attributes.getValue(Attributes.Name.CLASS_PATH),
@@ -511,7 +516,7 @@ pathTree:   for (int j=0; ; j++) {
     }
 
     /**
-     * Puts the given file in the given map. IF a value was already associated to the given file,
+     * Puts the given file in the given map. If a value was already associated to the given file,
      * then that value is preserved.
      *
      * @param  files The map in which to add the file, or {@code null} if not yet created.
@@ -531,7 +536,7 @@ pathTree:   for (int j=0; ; j++) {
 
     /**
      * If a file path in the given node or any children follow the Maven pattern, remove the
-     * artefact name and version numbers redundancies in order to make the name more compact.
+     * artifact name and version numbers redundancies in order to make the name more compact.
      * For example this method replaces {@code "org/opengis/geoapi/3.0.0/geoapi-3.0.0.jar"}
      * by {@code "org/opengis/(…)/geoapi-3.0.0.jar"}.
      */
@@ -596,9 +601,10 @@ pathTree:   for (int j=0; ; j++) {
      * Concatenates the given strings in the format "main (complement)".
      * Any of the given strings can be null.
      *
-     * @param main        The main string to show first.
-     * @param complement  The string to show after the main one.
-     * @param parenthesis {@code true} for writing the complement between parenthesis.
+     * @param  main        The main string to show first, or {@code null}.
+     * @param  complement  The string to show after the main one, or {@code null}.
+     * @param  parenthesis {@code true} for writing the complement between parenthesis, or {@code null}.
+     * @return The concatenated string, or {@code null} if all components are null.
      */
     private static CharSequence concatenate(final CharSequence main, final CharSequence complement, final boolean parenthesis) {
         if (main != null && main.length() != 0) {

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -19,6 +19,7 @@ package org.apache.sis.setup;
 import java.util.Map;
 import java.util.HashMap;
 import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
 import java.io.Serializable;
 import java.io.ObjectStreamException;
 import org.apache.sis.util.ArgumentChecks;
@@ -26,14 +27,39 @@ import org.apache.sis.util.logging.Loggi
 
 
 /**
- * Keys in a map of options, together with static constants for commonly-used options.
- * Developers can subclass this class for defining their own options.
+ * Keys in a map of options for configuring various services
+ * ({@link org.apache.sis.storage.DataStore}, <i>etc</i>).
+ * {@code OptionKey}s are used for aspects that usually do not need to be configured, except in a few specialized cases.
+ * For example most data file formats read by SIS do not require the user to specify the character encoding, since the
+ * encoding it is often given in the file header or in the format specification. However if SIS may have to read plain
+ * text files <em>and</em> the default platform encoding is not suitable, then the user can specify the desired encoding
+ * explicitely using the {@link #ENCODING} option.
+ *
+ * <p>All options are <em>hints</em> and may be silently ignored. For example most {@code DataStore}s will ignore the
+ * {@code ENCODING} option if irrelevant to their format, or if the encoding is specified in the data file header.</p>
+ *
+ * <p>Options are <em>transitive</em>: if a service uses others services for its internal working, the given options
+ * may also be given to those dependencies, at implementation choice.</p>
+ *
+ * {@section Defining new options}
+ * Developers who wish to define their own options can define static constants in a subclass,
+ * as in the following example:
+ *
+ * {@preformat java
+ *     public final class MyOptionKey<T> extends OptionKey<T> {
+ *         public static final OptionKey<String> MY_OPTION = new MyOptionKey<>("MY_OPTION", String.class);
+ *
+ *         private MyOptionKey(final String name, final Class<T> type) {
+ *             super(name, type);
+ *         }
+ *     }
+ * }
  *
  * @param <T> The type of option values.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.3
- * @version 0.3
+ * @version 0.4
  * @module
  */
 public class OptionKey<T> implements Serializable {
@@ -43,12 +69,25 @@ public class OptionKey<T> implements Ser
     private static final long serialVersionUID = -7580514229639750246L;
 
     /**
+     * The character encoding of document content.
+     * This option can be used when the file to read does not describe itself its encoding.
+     * For example this option can be used when reading plain text files, but is ignored when
+     * reading XML files having a {@code <?xml version="1.0" encoding="…"?>} declaration.
+     *
+     * <p>If this option is not provided, then the default value is the
+     * {@link Charset#defaultCharset() platform default}.</p>
+     *
+     * @since 0.4
+     */
+    public static final OptionKey<Charset> ENCODING = new OptionKey<Charset>("ENCODING", Charset.class);
+
+    /**
      * The encoding of a URL (<strong>not</strong> the encoding of the document content).
      * This option may be used when converting a {@link String} or a {@link java.net.URL}
-     * to a {@link java.net.URI} or a {@link java.io.File}:
+     * to a {@link java.net.URI} or a {@link java.io.File}. The following rules apply:
      *
      * <ul>
-     *   <li>URI are always encoded in UTF-8.</li>
+     *   <li>URI are always encoded in UTF-8. Consequently this option is ignored for URI.</li>
      *   <li>URL are often encoded in UTF-8, but not necessarily. Other encodings are possible
      *       (while not recommended), or some URL may not be encoded at all.</li>
      * </ul>
@@ -75,6 +114,25 @@ public class OptionKey<T> implements Ser
     public static final OptionKey<String> URL_ENCODING = new OptionKey<String>("URL_ENCODING", String.class);
 
     /**
+     * Whether a storage object (e.g. a {@link org.apache.sis.storage.DataStore}) shall be opened in read,
+     * write, append or other modes. The main options that can be provided are:
+     *
+     * <table class="sis">
+     *   <tr><th>Value</th>                             <th>Meaning</th></tr>
+     *   <tr><td>{@code "READ"}</td>   <td>Open for reading data from the storage object.</td></tr>
+     *   <tr><td>{@code "WRITE"}</td>  <td>Open for modifying existing data in the storage object.</td></tr>
+     *   <tr><td>{@code "APPEND"}</td> <td>Open for appending new data in the storage object.</td></tr>
+     *   <tr><td>{@link "CREATE"}</td> <td>Creates a new storage object (file or database) if it does not exist.</td></tr>
+     * </table>
+     *
+     * {@section Differences between the JDK6 and JDK7 branches of SIS}
+     * In the JDK7 branch of SIS, the array type for this key is {@code java.nio.file.OpenOption[]} instead than
+     * {@code Object[]} and the constants listed in the above table are {@code java.nio.file.StandardOpenOption}
+     * enumeration values.
+     */
+    public static final OptionKey<Object[]> OPEN_OPTIONS = new OptionKey<Object[]>("OPEN_OPTIONS", Object[].class);
+
+    /**
      * The byte buffer to use for input/output operations. Some {@link org.apache.sis.storage.DataStore}
      * implementations allow a byte buffer to be specified, thus allowing users to choose the buffer
      * {@linkplain ByteBuffer#capacity() capacity}, whether the buffer {@linkplain ByteBuffer#isDirect()

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Exceptions.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -16,8 +16,8 @@
  */
 package org.apache.sis.util;
 
-import java.util.Set;
-import java.util.HashSet;
+import java.util.List;
+import java.util.ArrayList;
 import java.util.Locale;
 import java.sql.SQLException;
 import org.apache.sis.internal.util.LocalizedException;
@@ -33,7 +33,7 @@ import org.apache.sis.internal.jdk7.JDK7
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @since   0.3 (derived from geotk-2.0)
- * @version 0.3
+ * @version 0.4
  * @module
  */
 public final class Exceptions extends Static {
@@ -127,34 +127,24 @@ public final class Exceptions extends St
      *         and no exception provide a message.
      */
     public static String formatChainedMessages(final Locale locale, String header, Throwable cause) {
-        Set<CharSequence> done = null;
+        List<String> previousLines = null;
         String lineSeparator = null;
         StringBuilder buffer = null;
         while (cause != null) {
             final String message = trimWhitespaces(getLocalizedMessage(cause, locale));
             if (message != null && !message.isEmpty()) {
                 if (buffer == null) {
-                    done = new HashSet<CharSequence>();
                     buffer = new StringBuilder(128);
                     lineSeparator = JDK7.lineSeparator();
+                    previousLines = new ArrayList<String>(4);
                     header = trimWhitespaces(header);
                     if (header != null && !header.isEmpty()) {
                         buffer.append(header);
-                        done.add(header);
-                        /*
-                         * The folowing is for avoiding to repeat the same message in the
-                         * common case where the header contains the exception class name
-                         * followed by the message, as in:
-                         *
-                         * FooException: InnerException: the inner message.
-                         */
-                        int s=0;
-                        while ((s=header.indexOf(':', s)) >= 0) {
-                            done.add(trimWhitespaces(header, ++s, header.length()));
-                        }
+                        previousLines.add(header);
                     }
                 }
-                if (done.add(message)) {
+                if (!contains(previousLines, message)) {
+                    previousLines.add(message);
                     if (buffer.length() != 0) {
                         buffer.append(lineSeparator);
                     }
@@ -175,4 +165,17 @@ public final class Exceptions extends St
         }
         return header;
     }
+
+    /**
+     * Returns {@code true} if a previous line contains the given exception message.
+     */
+    private static boolean contains(final List<String> previousLines, final String message) {
+        for (int i=previousLines.size(); --i>=0;) {
+            final int p = previousLines.get(i).indexOf(message);
+            if (p >= 0) {
+                return true;
+            }
+        }
+        return false;
+    }
 }

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Numbers.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -483,15 +483,16 @@ public final class Numbers extends Stati
     }
 
     /**
-     * Casts a number to the specified class. The class must by one of {@link Byte},
+     * Casts a number to the specified type. The target type can be one of {@link Byte},
      * {@link Short}, {@link Integer}, {@link Long}, {@link Float}, {@link Double},
      * {@link BigInteger} or {@link BigDecimal}.
      * This method makes the following choice:
      *
      * <ul>
-     *   <li>If the given type is {@code Double.class}, then this method returns
+     *   <li>If the given value is {@code null} or an instance of the given type, then it is returned unchanged.</li>
+     *   <li>Otherwise if the given type is {@code Double.class}, then this method returns
      *       <code>{@linkplain Double#valueOf(double) Double.valueOf}(number.doubleValue())</code>;</li>
-     *   <li>If the given type is {@code Float.class}, then this method returns
+     *   <li>Otherwise if the given type is {@code Float.class}, then this method returns
      *       <code>{@linkplain Float#valueOf(float) Float.valueOf}(number.floatValue())</code>;</li>
      *   <li>And likewise for all remaining known types.</li>
      * </ul>
@@ -614,7 +615,7 @@ public final class Numbers extends Stati
      * @return The value object, or {@code null} if {@code value} was null.
      * @throws IllegalArgumentException if {@code type} is not a recognized type.
      * @throws NumberFormatException if {@code type} is a subclass of {@link Number} and the
-     *         string value is not parseable as a number of the specified type.
+     *         string value is not parsable as a number of the specified type.
      */
     @SuppressWarnings("unchecked")
     public static <T> T valueOf(final String value, final Class<T> type)

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Static.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Static.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Static.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Static.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -58,6 +58,8 @@ package org.apache.sis.util;
  * <tr><th colspan="2" class="hsep">Input / Output (including CRS, XML, images)</th></tr>
  * <tr><td>{@link org.apache.sis.io.IO}</td>
  *     <td>Methods working on {@link Appendable} instances.</td></tr>
+ * <tr><td>{@link org.apache.sis.storage.DataStores}</td>
+ *     <td>Read or write geospatial data in various backends.</td></tr>
  * <tr><td>{@link org.apache.sis.xml.XML}</td>
  *     <td>Marshal or unmarshal ISO 19115 objects.</td></tr>
  * <tr><td>{@link org.apache.sis.xml.Namespaces}</td>

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Version.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Version.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Version.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/Version.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -18,6 +18,7 @@ package org.apache.sis.util;
 
 import java.io.Serializable;
 import java.util.StringTokenizer;
+import org.apache.sis.util.resources.Errors;
 
 
 /**
@@ -31,9 +32,9 @@ import java.util.StringTokenizer;
  * <p>This class provides methods for performing comparisons of {@code Version} objects where major,
  * minor and revision parts are compared as numbers when possible, or as strings otherwise.</p>
  *
- * @author  Martin Desruisseaux (IRD)
+ * @author  Martin Desruisseaux (IRD, Geomatys)
  * @since   0.3 (derived from geotk-2.4)
- * @version 0.3
+ * @version 0.4
  * @module
  */
 @Immutable
@@ -44,15 +45,26 @@ public class Version implements CharSequ
     private static final long serialVersionUID = 8402041502662929792L;
 
     /**
+     * The separator characters between {@linkplain #getMajor() major}, {@linkplain #getMinor() minor}
+     * and {@linkplain #getRevision() revision} components. Any character in this string fits.
+     */
+    private static final String SEPARATORS = ".-";
+
+    /**
      * The version of this Apache SIS distribution.
      */
-    public static final Version SIS = new Version("0.3-SNAPSHOT");
+    public static final Version SIS = new Version("0.4-SNAPSHOT");
 
     /**
-     * The separator characters between {@linkplain #getMajor() major}, {@linkplain #getMinor() minor}
-     * and {@linkplain #getRevision() revision} components. Any character in this string fits.
+     * A few commonly used version numbers. This list is based on SIS needs, e.g. in {@code DataStore}
+     * implementations. New constants are likely to be added in any future SIS versions.
+     *
+     * @see #valueOf(int[])
      */
-    private static final String SEPARATORS = ".-";
+    private static final Version[] CONSTANTS = {
+        new Version("1"),
+        new Version("2")
+    };
 
     /**
      * The version in string form, with leading and trailing spaces removed.
@@ -81,7 +93,56 @@ public class Version implements CharSequ
      */
     public Version(final String version) {
         ArgumentChecks.ensureNonNull("version", version);
-        this.version = CharSequences.trimWhitespaces(version);
+        this.version = version;
+    }
+
+    /**
+     * Returns an instance for the given integer values.
+     * The {@code components} array must contain at least 1 element, where:
+     *
+     * <ul>
+     *   <li>The first element is the {@linkplain #getMajor() major} number.</li>
+     *   <li>The second element (if any) is the {@linkplain #getMinor() minor} number.</li>
+     *   <li>The third element (if any) is the {@linkplain #getRevision() revision} number.</li>
+     *   <li>Other elements (if any) will be appended to the {@link #toString() string value}.</li>
+     * </ul>
+     *
+     * @param  components The major number, optionally followed by minor, revision or other numbers.
+     * @return A new or existing instance of {@code Version} for the given numbers.
+     *
+     * @since 0.4
+     */
+    public static Version valueOf(final int... components) {
+        if (components.length == 0) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "components"));
+        }
+        final Version version;
+        final int major = components[0];
+        if (components.length == 1) {
+            if (major >= 1 && major <= CONSTANTS.length) {
+                return CONSTANTS[major-1];
+            } else {
+                version = new Version(Integer.toString(major));
+            }
+        } else {
+            final StringBuilder buffer = new StringBuilder().append(major);
+            for (int i=1; i<components.length; i++) {
+                buffer.append('.').append(components[i]);
+            }
+            version = new Version(buffer.toString());
+        }
+        /*
+         * Pre-compute the 'parsed' array since we already have the integer values. It will avoid the need to
+         * create the 'this.components' array and to parse the String values if a 'getFoo()' method is invoked.
+         * Note that the cost is typically only the 'parsed' array creation, not Integer objects creation, since
+         * version numbers are usually small enough for allowing 'Integer.valueOf(int)' to cache them.
+         */
+        final Integer[] parsed = new Integer[components.length];
+        for (int i=0; i<components.length; i++) {
+            parsed[i] = components[i];
+        }
+        version.parsed = parsed;
+        return version;
     }
 
     /**

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/CodeListSet.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/CodeListSet.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/CodeListSet.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/CodeListSet.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -126,7 +126,7 @@ public class CodeListSet<E extends CodeL
      * those new elements will <em>not</em> be in this set.
      *
      * @param  elementType The type of code list elements to be included in this set.
-     * @param  fill {@code true} for filling the set with all known elements if the given type,
+     * @param  fill {@code true} for filling the set with all known elements of the given type,
      *         or {@code false} for leaving the set empty.
      * @throws IllegalArgumentException If the given class is not final.
      */

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -17,6 +17,7 @@
 package org.apache.sis.util.collection;
 
 import java.util.Arrays;
+import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.List;
@@ -38,13 +39,11 @@ import org.apache.sis.util.Workaround;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.resources.Vocabulary;
 import org.apache.sis.internal.util.LocalizedParseException;
 
 import static org.apache.sis.util.Characters.NO_BREAK_SPACE;
 
-// Related to JDK7
-import org.apache.sis.internal.jdk7.JDK7;
-
 
 /**
  * A parser and formatter for {@link TreeTable} instances.
@@ -82,6 +81,16 @@ import org.apache.sis.internal.jdk7.JDK7
  * then insert the {@code "……"} string, repeat the {@code '…'} character as many time as needed
  * (may be zero), and finally insert a space</cite>".
  *
+ * {@section Safety against infinite recursivity}
+ * Some {@code TreeTable} implementations generate the nodes dynamically as wrappers around Java objects.
+ * Such Java objects may contain cyclic associations (<var>A</var> contains <var>B</var> contains <var>C</var>
+ * contains <var>A</var>), which result in a tree of infinite depth. Some examples can been found in ISO 19115
+ * metadata. This {@code TreeTableFormat} class contains a safety against such cycles. The algorithm is based
+ * on the assumption that for each node, the values and children are fully determined by the
+ * {@linkplain TreeTable.Node#getUserObject() user object}, if non-null. Consequently for each node <var>C</var>
+ * to be formatted, if the user object of that node is the same instance (in the sense of the {@code ==} operator)
+ * than the user object of a parent node <var>A</var>, then the children of the <var>C</var> node will not be formatted.
+ *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @since   0.3 (derived from geotk-2.0)
  * @version 0.3
@@ -145,6 +154,12 @@ public class TreeTableFormat extends Tab
     private transient String treeBlank, treeLine, treeCross, treeEnd;
 
     /**
+     * The map to be given to {@link Writer#parentObjects}, created when first needed
+     * and reused for subsequent formating.
+     */
+    private transient Map<Object,Object> parentObjects;
+
+    /**
      * Creates a new tree table format.
      *
      * @param locale   The locale to use for numbers, dates and angles formatting,
@@ -555,14 +570,33 @@ public class TreeTableFormat extends Tab
         private boolean[] isLast;
 
         /**
+         * The {@linkplain TreeTable.Node#getUserObject() user object} of the parent nodes.
+         * We use this map as a safety against infinite recursivity, on the assumption that
+         * the node content and children are fully determined by the user object.
+         *
+         * <p>User objects in this map will be compared by the identity comparator rather than by the
+         * {@code equals(Object)} method because the later may be costly and could itself be vulnerable
+         * to infinite recursivity if it has not been implemented defensively.</p>
+         *
+         * <p>We check the user object instead than the node itself because the same node instance can
+         * theoretically not appear twice with a different parent.</p>
+         */
+        private final Map<Object,Object> parentObjects;
+
+        /**
          * Creates a new instance which will write in the given appendable.
+         *
+         * @param out           Where to format the tree.
+         * @param column        The columns of the tree table to format.
+         * @param parentObjects An initially empty {@link IdentityHashMap}.
          */
-        Writer(final Appendable out, final TableColumn<?>[] columns) {
+        Writer(final Appendable out, final TableColumn<?>[] columns, final Map<Object,Object> parentObjects) {
             super(columns.length >= 2 ? new TableAppender(out, "") : out);
             this.columns = columns;
             this.formats = getFormats(columns, false);
             this.values  = new Object[columns.length];
             this.isLast  = new boolean[8];
+            this.parentObjects = parentObjects;
             setTabulationExpanded(true);
             setLineSeparator(" ¶ ");
         }
@@ -570,10 +604,10 @@ public class TreeTableFormat extends Tab
         /**
          * Appends a textual representation of the given value.
          *
-         * @param  format The format to use.
+         * @param  format The format to use for the column as a whole, or {@code null} if unknown.
          * @param  value  The value to format (may be {@code null}).
          */
-        private void formatValue(final Format format, final Object value) throws IOException {
+        private void formatValue(Format format, final Object value) throws IOException {
             final CharSequence text;
             if (value == null) {
                 text = " "; // String for missing value.
@@ -585,12 +619,21 @@ public class TreeTableFormat extends Tab
                 text = format.format(value);
             } else if (value instanceof InternationalString) {
                 text = ((InternationalString) value).toString(getLocale());
+            } else if (value instanceof CharSequence) {
+                text = value.toString();
             } else if (value instanceof CodeList<?>) {
                 text = Types.getCodeTitle((CodeList<?>) value).toString(getLocale());
             } else if (value instanceof Enum<?>) {
                 text = CharSequences.upperCaseToSentence(((Enum<?>) value).name());
             } else {
-                text = String.valueOf(value);
+                /*
+                 * Check for a value-by-value format only as last resort.
+                 * If a column-wide format was given in argument to this method,
+                 * that format should have been used by above code in order to
+                 * produce a more uniform formatting.
+                 */
+                format = getFormat(value.getClass());
+                text = (format != null) ? format.format(value) : value.toString();
             }
             append(text);
         }
@@ -634,13 +677,38 @@ public class TreeTableFormat extends Tab
             if (level >= isLast.length) {
                 isLast = Arrays.copyOf(isLast, level*2);
             }
-            final Iterator<? extends TreeTable.Node> it = node.getChildren().iterator();
-            boolean hasNext = it.hasNext();
-            while (hasNext) {
-                final TreeTable.Node child = it.next();
-                hasNext = it.hasNext();
-                isLast[level] = !hasNext; // Must be set before the call to 'format' below.
-                format(child, level+1);
+            /*
+             * Format the children only if we do not detect an infinite recursivity. Our recursivity detection
+             * algorithm assumes that the node content is fully determined by the user object. If that assumption
+             * holds, then we have an infinite recursivity if the user object of the current node is also the user
+             * object of a parent node.
+             *
+             * Note that the value stored in the 'parentObjects' map needs to be the 'userObject' because we want
+             * the map value to be null if the user object is null, in order to format the children even if many
+             * null user objects exist in the tree.
+             */
+            final Object userObject = node.getUserObject();
+            if (parentObjects.put(userObject, userObject) == null) {
+                final Iterator<? extends TreeTable.Node> it = node.getChildren().iterator();
+                boolean hasNext = it.hasNext();
+                while (hasNext) {
+                    final TreeTable.Node child = it.next();
+                    hasNext = it.hasNext();
+                    isLast[level] = !hasNext; // Must be set before the call to 'format' below.
+                    format(child, level+1);
+                }
+                parentObjects.remove(userObject);
+            } else {
+                /*
+                 * Detected a recursivity. Format "(cycle omitted)" just below the node.
+                 */
+                for (int i=0; i<level; i++) {
+                    out.append(getTreeSymbols(true, isLast[i]));
+                }
+                final Locale locale = getLocale();
+                out.append('(').append(Vocabulary.getResources(locale)
+                   .getString(Vocabulary.Keys.CycleOmitted).toLowerCase(locale))
+                   .append(')').append(lineSeparator);
             }
         }
     }
@@ -671,8 +739,15 @@ public class TreeTableFormat extends Tab
             final List<TableColumn<?>> c = tree.getColumns();
             columns = c.toArray(new TableColumn<?>[c.size()]);
         }
-        final Writer out = new Writer(toAppendTo, columns);
-        out.format(tree.getRoot(), 0);
-        out.flush();
+        if (parentObjects == null) {
+            parentObjects = new IdentityHashMap<Object,Object>();
+        }
+        try {
+            final Writer out = new Writer(toAppendTo, columns, parentObjects);
+            out.format(tree.getRoot(), 0);
+            out.flush();
+        } finally {
+            parentObjects.clear();
+        }
     }
 }

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/collection/WeakValueHashMap.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -42,8 +42,12 @@ import org.apache.sis.internal.jdk7.Obje
  * A hashtable-based map implementation that uses {@linkplain WeakReference weak references},
  * leaving memory when an entry is not used anymore. An entry in a {@code WeakValueHashMap}
  * will automatically be removed when its value is no longer in ordinary use. This class is
- * similar to the standard {@link java.util.WeakHashMap} class provided in J2SE, except that
- * weak references are hold on values instead of keys.
+ * similar to the standard {@link java.util.WeakHashMap} class, except that weak references
+ * apply to values rather than keys.
+ *
+ * <p>Note that this class is <strong>not</strong> a cache, because the entries are discarded
+ * as soon as the garbage collector determines that they are no longer in use. If caching
+ * service are wanted, or if concurrency are wanted, consider using {@link Cache} instead.</p>
  *
  * <p>This class is convenient for avoiding the creation of duplicated elements, as in the
  * example below:</p>
@@ -60,20 +64,19 @@ import org.apache.sis.internal.jdk7.Obje
  *     }
  * }
  *
- * The calculation of a new value should be fast, because it is performed inside a synchronized
- * statement blocking all other access to the map. This is okay if that particular map instance
+ * In the above example, the calculation of a new value needs to be fast because it is performed inside a synchronized
+ * statement blocking all other access to the map. This is okay if that particular {@code WeakValueHashMap} instance
  * is not expected to be used in a highly concurrent environment.
  *
- * <p>Note that this class is <strong>not</strong> a cache, because the entries are discarded
- * as soon as the garbage collector determines that they are no longer in use. If caching
- * service are wanted, or if concurrency are wanted, consider using {@link Cache} instead.</p>
+ * <p>{@code WeakValueHashMap} works with array keys as one would expect. For example arrays of {@code int[]} are
+ * compared using the {@link java.util.Arrays#equals(int[], int[])} method.</p>
  *
  * @param <K> The class of key elements.
  * @param <V> The class of value elements.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @since   0.3 (derived from geotk-2.0)
- * @version 0.3
+ * @version 0.4
  * @module
  *
  * @see java.util.WeakHashMap
@@ -83,6 +86,23 @@ import org.apache.sis.internal.jdk7.Obje
 @ThreadSafe
 public class WeakValueHashMap<K,V> extends AbstractMap<K,V> {
     /**
+     * Comparison mode for key objects. The standard mode is {@code EQUALS}, which means that keys are compared
+     * using their {@link Object#equals(Object)} method. But {@code WeakValueHashMap} will automatically select
+     * {@code DEEP_EQUALS} if there is a chance that some keys are arrays. In the later case, comparisons will
+     * be done by the more costly {@link Objects#deepEquals(Object, Object)} method instead.
+     *
+     * <p>The {@code IDENTITY} mode is rarely used, and is selected only if the user explicitely asks for this mode
+     * at construction time. This mode is provided because reference-equality semantic is sometime required, and
+     * hard to simulate if not supported natively by the hash map. See {@link java.util.IdentityHashMap} javadoc
+     * for some examples of cases where reference-equality semantic is useful.</p>
+     *
+     * @see #comparisonMode
+     * @see #keyEquals(Object, Object)
+     * @see #keyHashCode(Object)
+     */
+    private static final byte IDENTITY = 0, EQUALS = 1, DEEP_EQUALS = 2;
+
+    /**
      * An entry in the {@link WeakValueHashMap}. This is a weak reference
      * to a value together with a strong reference to a key.
      */
@@ -184,10 +204,15 @@ public class WeakValueHashMap<K,V> exten
     private final Class<K> keyType;
 
     /**
-     * {@code true} if the keys in this map may be arrays. If the keys can not be
-     * arrays, then we can avoid the calls to the costly {@link Utilities} methods.
+     * Whether keys shall be compared by reference-equality ({@link #IDENTITY}), by shallow object-equality
+     * ({@link #EQUALS}) or by deep object-equality ({@link #DEEP_EQUALS}). The {@code DEEP_EQUALS} mode is
+     * selected only if the keys in this map may be arrays. If the keys can not be arrays, then we select the
+     * {@code EQUALS} mode for avoiding calls to the costly {@link Objects#deepEquals(Object, Object)} method.
+     *
+     * @see #keyEquals(Object, Object)
+     * @see #keyHashCode(Object)
      */
-    private final boolean mayContainArrays;
+    private final byte comparisonMode;
 
     /**
      * The set of entries, created only when first needed.
@@ -208,8 +233,27 @@ public class WeakValueHashMap<K,V> exten
      * @param keyType The type of keys in the map.
      */
     public WeakValueHashMap(final Class<K> keyType) {
-        this.keyType           = keyType;
-        mayContainArrays       = keyType.isArray() || keyType.equals(Object.class);
+        this(keyType, false);
+    }
+
+    /**
+     * Creates a new {@code WeakValueHashMap}, optionally using reference-equality in place of object-equality.
+     * If {@code identity} is {@code true}, then two keys {@code k1} and {@code k2} are considered equal if and
+     * only if {@code (k1 == k2)} instead than if {@code k1.equals(k2)}.
+     *
+     * <p>Reference-equality semantic is rarely used. See the {@link java.util.IdentityHashMap} class javadoc
+     * for a discussion about drawbacks and use cases when reference-equality semantic is useful.</p>
+     *
+     * @param keyType  The type of keys in the map.
+     * @param identity {@code true} if the map shall use reference-equality in place of object-equality
+     *                 when comparing keys, or {@code false} for the standard behavior.
+     *
+     * @since 0.4
+     */
+    public WeakValueHashMap(final Class<K> keyType, final boolean identity) {
+        this.keyType   = keyType;
+        comparisonMode = identity ? IDENTITY :
+                (keyType.isArray() || keyType.equals(Object.class)) ? DEEP_EQUALS : EQUALS;
         lastTimeNormalCapacity = System.nanoTime();
         /*
          * Workaround for the "generic array creation" compiler error.
@@ -277,16 +321,31 @@ public class WeakValueHashMap<K,V> exten
 
     /**
      * Returns the hash code value for the given key.
+     *
+     * @param key The key (can not be null).
      */
     final int keyHashCode(final Object key) {
-        return mayContainArrays ? Utilities.deepHashCode(key) : key.hashCode();
+        switch (comparisonMode) {
+            case IDENTITY:    return System.identityHashCode(key);
+            case EQUALS:      return key.hashCode();
+            case DEEP_EQUALS: return Utilities.deepHashCode(key);
+            default: throw new AssertionError(comparisonMode);
+        }
     }
 
     /**
      * Returns {@code true} if the two given keys are equal.
+     *
+     * @param k1 The first key (can not be null).
+     * @paral k2 The second key.
      */
     final boolean keyEquals(final Object k1, final Object k2) {
-        return mayContainArrays ? Objects.deepEquals(k1, k2) : k1.equals(k2);
+        switch (comparisonMode) {
+            case IDENTITY:    return k1 == k2;
+            case EQUALS:      return k1.equals(k2);
+            case DEEP_EQUALS: return Objects.deepEquals(k1, k2);
+            default: throw new AssertionError(comparisonMode);
+        }
     }
 
     /**

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/iso/AbstractFactory.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/iso/AbstractFactory.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/iso/AbstractFactory.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/iso/AbstractFactory.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -40,6 +40,10 @@ public abstract class AbstractFactory im
      * Returns the implementor of this factory, or {@code null} if unknown.
      * The default implementation tries to fetch this information from the
      * manifest associated to the package of this class.
+     *
+     * @return The vendor for this factory implementation, or {@code null} if unknown.
+     *
+     * @see Package#getImplementationVendor()
      */
     @Override
     public Citation getVendor() {

Modified: sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/iso/AbstractName.java
URL: http://svn.apache.org/viewvc/sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/iso/AbstractName.java?rev=1517321&r1=1517320&r2=1517321&view=diff
==============================================================================
--- sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/iso/AbstractName.java [UTF-8] (original)
+++ sis/branches/Shapefile/core/sis-utility/src/main/java/org/apache/sis/util/iso/AbstractName.java [UTF-8] Sun Aug 25 15:49:51 2013
@@ -19,6 +19,7 @@ package org.apache.sis.util.iso;
 import java.util.List;
 import java.util.Locale;
 import java.util.Iterator;
+import java.util.ConcurrentModificationException;
 import java.io.Serializable;
 import javax.xml.bind.annotation.XmlType;
 import org.opengis.util.NameSpace;
@@ -26,6 +27,7 @@ import org.opengis.util.LocalName;
 import org.opengis.util.ScopedName;
 import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
+import org.apache.sis.internal.system.DefaultFactories;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.Immutable;
 
@@ -89,6 +91,51 @@ public abstract class AbstractName imple
     }
 
     /**
+     * Returns a SIS name implementation with the values of the given arbitrary implementation.
+     * This method performs the first applicable actions in the following choices:
+     *
+     * <ul>
+     *   <li>If the given object is {@code null}, then this method returns {@code null}.</li>
+     *   <li>Otherwise if the given object is already an instance of {@code AbstractName},
+     *       then it is returned unchanged.</li>
+     *   <li>Otherwise if the given object is an instance of {@link LocalName}, then this
+     *       method delegates to {@link DefaultLocalName#castOrCopy(LocalName)}.</li>
+     *   <li>Otherwise a new instance of an {@code AbstractName} subclass is created using the
+     *       {@link DefaultNameFactory#createGenericName(NameSpace, CharSequence[])} method.</li>
+     * </ul>
+     *
+     * @param  object The object to get as a SIS implementation, or {@code null} if none.
+     * @return A SIS implementation containing the values of the given object (may be the
+     *         given object itself), or {@code null} if the argument was null.
+     */
+    public static AbstractName castOrCopy(final GenericName object) {
+        if (object == null || object instanceof AbstractName) {
+            return (AbstractName) object;
+        }
+        if (object instanceof LocalName) {
+            return DefaultLocalName.castOrCopy((LocalName) object);
+        }
+        /*
+         * Recreates a new name for the given name in order to get
+         * a SIS implementation from an arbitrary implementation.
+         */
+        final List<? extends LocalName> parsedNames = object.getParsedNames();
+        final CharSequence[] names = new CharSequence[parsedNames.size()];
+        int i=0;
+        for (final LocalName component : parsedNames) {
+            names[i++] = component.toInternationalString();
+        }
+        if (i != names.length) {
+            throw new ConcurrentModificationException(Errors.format(Errors.Keys.UnexpectedChange_1, "parsedNames"));
+        }
+        /*
+         * Following cast should be safe because the SIS_NAMES factory is fixed to a
+         * DefaultNameFactory instance, which is known to create AbstractName instances.
+         */
+        return (AbstractName) DefaultFactories.SIS_NAMES.createGenericName(object.scope(), names);
+    }
+
+    /**
      * Returns the scope (name space) in which this name is local. For example if a
      * {@linkplain #toFullyQualifiedName() fully qualified name} is {@code "org.opengis.util.Record"}
      * and if this instance is the {@code "util.Record"} part, then its scope is
@@ -98,6 +145,8 @@ public abstract class AbstractName imple
      * no scope. If this method is invoked on such name, then the SIS implementation returns a
      * global scope instance (i.e. an instance for which {@link DefaultNameSpace#isGlobal()}
      * returns {@code true}) which is unique and named {@code "global"}.</p>
+     *
+     * @return The scope of this name.
      */
     @Override
     public abstract NameSpace scope();
@@ -105,6 +154,8 @@ public abstract class AbstractName imple
     /**
      * Indicates the number of levels specified by this name. The default implementation returns
      * the size of the list returned by the {@link #getParsedNames()} method.
+     *
+     * @return The depth of this name.
      */
     @Override
     public int depth() {
@@ -124,6 +175,9 @@ public abstract class AbstractName imple
      * Returns the sequence of {@linkplain DefaultLocalName local names} making this generic name.
      * The length of this sequence is the {@linkplain #depth() depth}. It does not include the
      * {@linkplain #scope() scope}.
+     *
+     * @return The local names making this generic name, without the {@linkplain #scope() scope}.
+     *         Shall never be {@code null} neither empty.
      */
     @Override
     public abstract List<? extends LocalName> getParsedNames();
@@ -132,9 +186,10 @@ public abstract class AbstractName imple
      * Returns the first element in the sequence of {@linkplain #getParsedNames() parsed names}.
      * For any {@code LocalName}, this is always {@code this}.
      *
-     * <p><b>Example</b>:
-     * If {@code this} name is {@code "org.opengis.util.Record"} (no matter its
-     * {@linkplain #scope() scope}), then this method returns {@code "org"}.</p>
+     * {@example If <code>this</code> name is <code>"org.opengis.util.Record"</code>
+     *           (no matter its scope, then this method returns <code>"org"</code>.}
+     *
+     * @return The first element in the list of {@linkplain #getParsedNames() parsed names}.
      */
     @Override
     public LocalName head() {
@@ -145,9 +200,10 @@ public abstract class AbstractName imple
      * Returns the last element in the sequence of {@linkplain #getParsedNames() parsed names}.
      * For any {@code LocalName}, this is always {@code this}.
      *
-     * <p><b>Example</b>:
-     * If {@code this} name is {@code "org.opengis.util.Record"} (no matter its
-     * {@linkplain #scope() scope}), then this method returns {@code "Record"}.</p>
+     * {@example If <code>this</code> name is <code>"org.opengis.util.Record"</code>
+     *           (no matter its scope, then this method returns <code>"Record"</code>.}
+     *
+     * @return The last element in the list of {@linkplain #getParsedNames() parsed names}.
      */
     @Override
     public LocalName tip() {
@@ -159,6 +215,8 @@ public abstract class AbstractName imple
      * Returns a view of this name as a fully-qualified name. The {@linkplain #scope() scope}
      * of a fully qualified name is {@linkplain DefaultNameSpace#isGlobal() global}.
      * If the scope of this name is already global, then this method returns {@code this}.
+     *
+     * @return The fully-qualified name (never {@code null}).
      */
     @Override
     public synchronized GenericName toFullyQualifiedName() {
@@ -181,6 +239,9 @@ public abstract class AbstractName imple
      * {@code this} name is {@code "util.Record"} and the given {@code scope} argument is
      * {@code "org.opengis"}, then {@code this.push(scope)} shall return
      * {@code "org.opengis.util.Record"}.
+     *
+     * @param  scope The name to use as prefix.
+     * @return A concatenation of the given scope with this name.
      */
     @Override
     public ScopedName push(final GenericName scope) {
@@ -219,6 +280,8 @@ public abstract class AbstractName imple
      *   <li><code>{@linkplain #tip()}.toString()</code> is guaranteed to not contain
      *       any scope.</li>
      * </ul>
+     *
+     * @return A local-independent string representation of this name.
      */
     @Override
     public synchronized String toString() {
@@ -246,6 +309,8 @@ public abstract class AbstractName imple
      * has been localized in the {@linkplain InternationalString#toString(Locale) specified locale}.
      * If no international string is available, then this method returns an implementation mapping
      * to {@link #toString()} for all locales.
+     *
+     * @return A localizable string representation of this name.
      */
     @Override
     public synchronized InternationalString toInternationalString() {
@@ -282,7 +347,7 @@ public abstract class AbstractName imple
          * @param asString The string representation of the enclosing abstract name.
          * @param parsedNames The value returned by {@link AbstractName#getParsedNames()}.
          */
-        public International(final String asString, final List<? extends LocalName> parsedNames) {
+        International(final String asString, final List<? extends LocalName> parsedNames) {
             super(asString);
             this.parsedNames = parsedNames;
         }
@@ -318,6 +383,14 @@ public abstract class AbstractName imple
             }
             return false;
         }
+
+        /**
+         * Returns a hash code value for this international text.
+         */
+        @Override
+        public int hashCode() {
+            return parsedNames.hashCode() ^ (int) serialVersionUID;
+        }
     }
 
     /**



Mime
View raw message