You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by tl...@apache.org on 2013/08/25 17:49:59 UTC
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/...
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;
+ }
}
/**