Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/TypeRegistration.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/TypeRegistration.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/TypeRegistration.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/TypeRegistration.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -16,8 +16,11 @@ */ package org.apache.sis.internal.jaxb; +import java.util.Map; +import java.util.HashMap; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import javax.xml.bind.JAXBContext; @@ -47,14 +50,37 @@ import org.apache.sis.internal.system.De */ public abstract class TypeRegistration { /** + * Undocumented (for now) marshaller property for specifying conversions to apply on root objects + * before marshalling. Conversions are applied by the {@link #toImplementation(Object)} method. + * + * @see #addDefaultRootAdapters(Map) + */ + public static final String ROOT_ADAPTERS = "org.apache.sis.xml.rootAdapters"; + + /** * The JAXB context, or {@code null} if not yet created or if the classpath changed. + * + * @see #getSharedContext() */ private static Reference<JAXBContext> context; + + /** + * The {@link TypeRegistration} instances found on the classpath for which the + * {@link #toImplementation(Object)} method has been overridden. + * + * @see #addDefaultRootAdapters(Map) + */ + private static TypeRegistration[] converters; + + /** + * Forces reloading of JAXB context and converters if the classpath changes. + */ static { SystemListener.add(new SystemListener(Modules.UTILITIES) { @Override protected void classpathChanged() { synchronized (TypeRegistration.class) { - context = null; + context = null; + converters = null; } } }); @@ -67,39 +93,83 @@ public abstract class TypeRegistration { } /** - * Adds to the given collection every types that should be given to - * the initial JAXB context. + * Adds to the given collection every types that should be given to the initial JAXB context. + * The types added by this method include only implementation classes having JAXB annotations. + * If the module can also marshal arbitrary implementations of some interfaces (e.g. GeoAPI), + * then the {@link #canMarshalInterfaces()} method should be overridden. * * @param addTo the collection in which to add new types. */ - public abstract void getTypes(final Collection<Class<?>> addTo); + protected abstract void getTypes(final Collection<Class<?>> addTo); + + /** + * Returns {@code true} if the module can also marshal arbitrary implementation of some interfaces. + * If this method returns {@code true}, then the {@link #toImplementation(Object)} method shall be + * overridden. + * + * @return whether the module can also marshal arbitrary implementation of some interfaces. + * + * @since 0.8 + */ + protected boolean canMarshalInterfaces() { + return false; + } /** - * Returns the root classes of SIS objects to be marshalled by default. - * Those classes can be given as the last argument to the {@code MarshallerPool} - * constructors, in order to bound a default set of classes with {@code JAXBContext}. + * If the given value needs to be converted before marshalling, apply the conversion now. + * Otherwise returns {@code null} if the value class is not recognized, or {@code value} + * if the class is recognized but the value does not need to be changed. + * + * <p>Subclasses that override this method will typically perform an {@code instanceof} check, then + * invoke one of the {@code castOrCopy(…)} static methods defined in various Apache SIS classes.</p> + * + * <p>This method is invoked only if {@link #canMarshalInterfaces()} returns {@code true}.</p> * - * <p>The list of classes is determined dynamically from the SIS modules found on - * the classpath.</p> + * @param value the value to convert before marshalling. + * @return the value to marshall; or {@code null} if this method does not recognize the value class. + * @throws JAXBException if an error occurred while converting the given object. * - * @return the default set of classes to be bound to the {@code JAXBContext}. + * @since 0.8 */ - private static Class<?>[] defaultClassesToBeBound() { + public Object toImplementation(final Object value) throws JAXBException { + return null; + } + + /** + * Scans the classpath for root classes to put in JAXB context and for converters to those classes. + * Those lists are determined dynamically from the SIS modules found on the classpath. + * The list of root classes is created only if the {@code getTypes} argument is {@code true}. + * + * @param getTypes whether to get the root classes to put in JAXB context (may cause class loading). + * @return if {@code getTypes} was {@code true}, the root classes to be bound in {@code JAXBContext}. + */ + private static Class<?>[] load(final boolean getTypes) { /* * Implementation note: do not keep the ServiceLoader in static field because: * - * 1) It would cache the TypeRegistration instances, which are not needed after this method call. + * 1) It would cache more TypeRegistration instances than needed for this method call. * 2) The ClassLoader between different invocations may be different in an OSGi context. */ final ArrayList<Class<?>> types = new ArrayList<>(); - for (final TypeRegistration t : DefaultFactories.createServiceLoader(TypeRegistration.class)) { - t.getTypes(types); + final ArrayList<TypeRegistration> toImpl = (converters == null) ? new ArrayList<TypeRegistration>() : null; + if (toImpl != null || getTypes) { + for (final TypeRegistration t : DefaultFactories.createServiceLoader(TypeRegistration.class)) { + if (getTypes) { + t.getTypes(types); + } + if (toImpl != null && t.canMarshalInterfaces()) { + toImpl.add(t); + } + } + if (toImpl != null) { + converters = toImpl.toArray(new TypeRegistration[toImpl.size()]); + } } return types.toArray(new Class<?>[types.size()]); } /** - * Returns the shared {@code JAXBContext} for the set of {@link #defaultClassesToBeBound()}. + * Returns the shared {@code JAXBContext} for the set of {@link #load()}. * Note that the {@code JAXBContext} class is thread safe, but the {@code Marshaller}, * {@code Unmarshaller}, and {@code Validator} classes are not thread safe. * @@ -114,8 +184,43 @@ public abstract class TypeRegistration { return instance; } } - final JAXBContext instance = JAXBContext.newInstance(defaultClassesToBeBound()); + final JAXBContext instance = JAXBContext.newInstance(load(true)); context = new WeakReference<>(instance); return instance; } + + /** + * Completes the given properties with an entry for {@link #ROOT_ADAPTERS} if not already present. + * If a {@code ROOT_ADAPTERS} entry is already present, then the map is returned unchanged. + * + * <p>This method store a direct reference to the internal {@code TypeRegistration[]} array in the given map. + * <strong>That array shall not be modified.</strong> This method is currently for Apache SIS internal usage only, + * because the {@code TypeRegistration} class is not part of public API. However if we add this functionality in a + * future SIS release (probably as an interface rather than exposing {@code TypeRegistration} itself), then we may + * consider removing this method.</p> + * + * @param properties the properties to complete. + * @return the given properties with the {@link #ROOT_ADAPTERS} entry added. + * + * @since 0.8 + */ + public static Map<String,?> addDefaultRootAdapters(final Map<String,?> properties) { + if (properties != null && properties.containsKey(ROOT_ADAPTERS)) { + return properties; + } + TypeRegistration[] c; + synchronized (TypeRegistration.class) { + c = converters; + if (c == null) { + load(false); + c = converters; + } + } + if (properties == null) { + return Collections.singletonMap(ROOT_ADAPTERS, c); + } + final Map<String,Object> copy = new HashMap<String,Object>(properties); + copy.put(ROOT_ADAPTERS, c); + return copy; + } }
Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/Constants.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -37,6 +37,14 @@ import org.apache.sis.util.Static; */ public final class Constants extends Static { /** + * The default indentation value to use in various text formats (both WKT and XML). + * We use a small value (2 instead of 4) because OGC's XML are very verbose. + * + * @see org.apache.sis.setup.OptionKey#INDENTATION + */ + public static final byte DEFAULT_INDENTATION = 2; + + /** * The {@value} code space. */ public static final String EPSG = "EPSG"; Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/StreamWriterDelegate.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/StreamWriterDelegate.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/StreamWriterDelegate.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/StreamWriterDelegate.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -72,20 +72,20 @@ public class StreamWriterDelegate implem /** Forwards the call verbatim. */ @Override - public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException { - out.writeEmptyElement(namespaceURI, localName); + public void writeEmptyElement(String localName) throws XMLStreamException { + out.writeEmptyElement(localName); } /** Forwards the call verbatim. */ @Override - public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { - out.writeEmptyElement(prefix, localName, namespaceURI); + public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException { + out.writeEmptyElement(namespaceURI, localName); } /** Forwards the call verbatim. */ @Override - public void writeEmptyElement(String localName) throws XMLStreamException { - out.writeEmptyElement(localName); + public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException { + out.writeEmptyElement(prefix, localName, namespaceURI); } /** Forwards the call verbatim. */ Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/Utilities.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/Utilities.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/Utilities.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/Utilities.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -191,8 +191,10 @@ public final class Utilities extends Sta if (precision >= 0) { for (int i=0,n=0; i<length; i += n) { if (--precision < 0) { - // Found the amount of characters to keep. The 'n' variable can be - // zero only if precision == 0, in which case the string is empty. + /* + * Found the amount of characters to keep. The 'n' variable can be + * zero only if precision == 0, in which case the string is empty. + */ if (n == 0) { value = ""; } else { Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPaths.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPaths.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPaths.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/internal/util/XPaths.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -71,7 +71,7 @@ scan: while (offset < length) { case '(': parenthesis++; break; case ')': parenthesis--; break; default: { - if (Character.isWhitespace(c)) break; // Not supposed to be valid, but be lenient. + if (Character.isSpaceChar(c)) break; // Not supposed to be valid, but be lenient. if (parenthesis != 0) break; break scan; // Non-valid character outside parenthesis. } Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/io/CompoundFormat.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/io/CompoundFormat.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/io/CompoundFormat.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/io/CompoundFormat.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -24,9 +24,9 @@ import java.util.Date; import java.io.IOException; import java.text.Format; import java.text.DateFormat; +import java.text.NumberFormat; import java.text.FieldPosition; import java.text.ParsePosition; -import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import javax.measure.Unit; @@ -40,11 +40,13 @@ import org.apache.sis.measure.UnitFormat import org.apache.sis.util.Localized; import org.apache.sis.util.ArraysExt; import org.apache.sis.util.ArgumentChecks; -import org.apache.sis.util.collection.BackingStoreException; import org.apache.sis.internal.util.LocalizedParseException; import static org.apache.sis.internal.util.StandardDateFormat.UTC; +// Branch-dependent imports +import org.apache.sis.internal.jdk8.UncheckedIOException; + /** * Base class of {@link Format} implementations which delegate part of their work to other @@ -73,7 +75,7 @@ import static org.apache.sis.internal.ut * throws a {@code ParseException} on error. This allows both substring parsing and more accurate exception message * in case of error.</div> * - * @param <T> The base type of objects parsed and formatted by this class. + * @param <T> the base type of objects parsed and formatted by this class. * * @author Martin Desruisseaux (Geomatys) * @since 0.3 @@ -171,7 +173,7 @@ public abstract class CompoundFormat<T> * @return the timezone used for this format, or UTC for unlocalized format. */ public TimeZone getTimeZone() { - return timezone != null ? (TimeZone) timezone.clone() : TimeZone.getTimeZone(UTC); + return (timezone != null) ? (TimeZone) timezone.clone() : TimeZone.getTimeZone(UTC); } /** @@ -214,13 +216,14 @@ public abstract class CompoundFormat<T> * </ul> * * <div class="note"><b>Example:</b> - * If parsing of the {@code "30.0 40,0"} coordinate fails on the coma in the last number, then the {@code pos} + * if parsing of the {@code "30.0 40,0"} coordinate fails on the coma in the last number, then the {@code pos} * error index will be set to 5 (the beginning of the {@code "40.0"} character sequence) while the * {@link ParseException} error offset will be set to 2 (the coma position relative the beginning * of the {@code "40.0"} character sequence).</div> * * This error offset policy is a consequence of the compound nature of {@code CompoundFormat}, - * since the exception may have been produced by a call to {@link Format#parseObject(String)}. + * since the exception may have been produced by a call to {@link Format#parseObject(String)} + * on one of the {@linkplain #getFormat(Class) sub-formats} used by this {@code CompoundFormat}. * * @param text the character sequence for the object to parse. * @param pos the position where to start the parsing. @@ -331,16 +334,16 @@ public abstract class CompoundFormat<T> @Override public StringBuffer format(final Object object, final StringBuffer toAppendTo, final FieldPosition pos) { final Class<? extends T> valueType = getValueType(); - ArgumentChecks.ensureCanCast("tree", valueType, object); + ArgumentChecks.ensureCanCast("object", valueType, object); try { format(valueType.cast(object), toAppendTo); } catch (IOException e) { /* * Should never happen when writing into a StringBuffer, unless the user - * override the format(Object, Appendable) method. We do not rethrown an + * override the format(Object, Appendable) method. We do not rethrow an * AssertionError because of this possibility. */ - throw new BackingStoreException(e); + throw new UncheckedIOException(e); } return toAppendTo; } Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/io/DefaultFormat.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/io/DefaultFormat.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/io/DefaultFormat.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/io/DefaultFormat.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -39,7 +39,7 @@ import org.apache.sis.internal.util.Loca * * @author Martin Desruisseaux (Geomatys) * @since 0.3 - * @version 0.3 + * @version 0.8 * @module */ @SuppressWarnings("CloneableClassWithoutClone") // Because this class does not contain field that need to be cloned. @@ -125,15 +125,39 @@ final class DefaultFormat extends Format */ @Override public Object parseObject(String source, final ParsePosition pos) { - final int length = source.length(); - final int index = CharSequences.skipLeadingWhitespaces(source, pos.getIndex(), length); - source = source.substring(index, CharSequences.skipTrailingWhitespaces(source, index, length)); + boolean exponent = false; + final int index = CharSequences.skipLeadingWhitespaces(source, pos.getIndex(), source.length()); + int end; + for (end = index; end < source.length(); end++) { + final char c = source.charAt(end); + switch (c) { + default: { + if (c >= '+' && c <= '9') continue; + break; + /* + * ASCII characters in above range are +,-./0123456789 + * But the , and / characters are excluded by the case below. + */ + } + case ',': case '/': break; + case 'E': case 'e': { + if (exponent) break; + exponent = true; + continue; + } + } + break; + } + source = source.substring(index, end); + final Object value; try { - return valueOf(source); + value = valueOf(source); } catch (NumberFormatException cause) { pos.setErrorIndex(index); return null; } + pos.setIndex(end); + return value; } /** Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/AbstractUnit.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -308,8 +308,11 @@ abstract class AbstractUnit<Q extends Qu /** * Returns {@code true} if the given Unicode code point is a valid character for a unit symbol. * Current implementation accepts letters, subscripts and the degree sign, but the set of legal - * characters may be expanded in any future SIS version. The most important goal is to avoid - * confusion with exponents and to detect where a unit symbol ends. + * characters may be expanded in any future SIS version (however it should never allow spaces). + * The goal is to avoid confusion with exponents and to detect where a unit symbol ends. + * + * <p>Space characters must be excluded from the set of legal characters because allowing them + * would make harder for {@link UnitFormat} to detect correctly where a unit symbol ends.</p> * * <p>Note that some units defined in the {@link Units} class break this rule. In particular, * some of those units contains superscripts or division sign. But the hard-coded symbols in Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -127,6 +127,7 @@ import org.apache.sis.internal.jdk8.JDK8 * @see Angle * @see Latitude * @see Longitude + * @see org.apache.sis.geometry.CoordinateFormat */ public class AngleFormat extends Format implements Localized { /** Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Latitude.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Latitude.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Latitude.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Latitude.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -52,6 +52,7 @@ package org.apache.sis.measure; * * @see Longitude * @see AngleFormat + * @see org.apache.sis.geometry.CoordinateFormat */ public final class Latitude extends Angle { /** Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/LinearConverter.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -105,6 +105,11 @@ final class LinearConverter extends Abst private transient volatile BigDecimal scale10, offset10; /** + * The inverse of this unit converter. Computed when first needed. + */ + private transient volatile LinearConverter inverse; + + /** * Creates a new linear converter for the given scale and offset. * The complete formula applied is {@code y = (x*scale + offset) / divisor}. */ @@ -253,7 +258,11 @@ final class LinearConverter extends Abst */ @Override public synchronized UnitConverter inverse() { - return isIdentity() ? this : new LinearConverter(divisor, -offset, scale); + if (inverse == null) { + inverse = isIdentity() ? this : new LinearConverter(divisor, -offset, scale); + inverse.inverse = this; + } + return inverse; } /** Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Longitude.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Longitude.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Longitude.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Longitude.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -37,6 +37,7 @@ package org.apache.sis.measure; * * @see Latitude * @see AngleFormat + * @see org.apache.sis.geometry.CoordinateFormat */ public final class Longitude extends Angle { /** Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/Range.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -182,7 +182,7 @@ public class Range<E extends Comparable< * by the {@link #create(Comparable, boolean, Comparable, boolean)} method - otherwise we may * get an {@link ArrayStoreException}. */ - @SuppressWarnings({"unchecked","rawtypes"}) // Generic array creation. + @SuppressWarnings({"unchecked","rawtypes"}) // Generic array creation. Range<E>[] newArray(final int length) { return new Range[length]; } Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -282,7 +282,7 @@ public class UnitFormat extends Format i * Mapping from long localized and unlocalized names to unit instances. * This map is used only for parsing and created when first needed. * - * @see #nameToUnit() + * @see #fromName(String) */ private transient volatile Map<String,Unit<?>> nameToUnit; @@ -291,7 +291,7 @@ public class UnitFormat extends Format i * if the user create many {@code UnitFormat} instances. Note that we do not cache {@link #symbolToName} because * {@link ResourceBundle} already provides its own caching mechanism. * - * @see #nameToUnit() + * @see #fromName(String) */ private static final WeakValueHashMap<Locale, Map<String,Unit<?>>> SHARED = new WeakValueHashMap<>(Locale.class); @@ -383,8 +383,9 @@ public class UnitFormat extends Format i * * <div class="section">Restriction on character set</div> * Current implementation accepts only {@linkplain Character#isLetter(int) letters}, - * {@linkplain Characters#isSubScript(int) subscripts}, {@linkplain Character#isWhitespace(int) whitespaces} - * and the degree sign (°), + * {@linkplain Characters#isSubScript(int) subscripts}, {@linkplain Character#isSpaceChar(int) spaces} + * (including non-breaking spaces but <strong>not</strong> CR/LF characters), the degree sign (°) and + * a few other characters like underscore, * but the set of legal characters may be expanded in future Apache SIS versions. * However the following restrictions are likely to remain: * @@ -405,7 +406,7 @@ public class UnitFormat extends Format i ArgumentChecks.ensureNonEmpty("label", label); for (int i=0; i < label.length();) { final int c = label.codePointAt(i); - if (!AbstractUnit.isSymbolChar(c) && !Character.isWhitespace(c)) { + if (!AbstractUnit.isSymbolChar(c) && !Character.isSpaceChar(c)) { // NOT Character.isWhitespace(int) throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "label", label)); } i += Character.charCount(c); @@ -444,16 +445,36 @@ public class UnitFormat extends Format i } /** - * Returns the mapping from long localized and unlocalized names to unit instances. - * This mapping is somewhat the converse of {@link #symbolToName()}, but includes + * Returns the unit instance for the given long (un)localized or name. + * This method is somewhat the converse of {@link #symbolToName()}, but recognizes also * international and American spelling of unit names in addition of localized names. * The intend is to recognize "meter" as well as "metre". * * <p>While we said that {@code UnitFormat} is not thread safe, we make an exception for this method * for allowing the singleton {@link #INSTANCE} to parse symbols in a multi-threads environment.</p> */ - @SuppressWarnings("ReturnOfCollectionOrArrayField") - private Map<String,Unit<?>> nameToUnit() { + @SuppressWarnings("fallthrough") + private Unit<?> fromName(String uom) { + /* + * Before to search in resource bundles, check for degrees units. The "deg" unit can be both angular + * and Celsius degrees. We try to resolve this ambiguity by looking for the "C" suffix. We perform a + * special case for the degrees units because SI symbols are case-sentive and unit names in resource + * bundles are case-insensitive, but the "deg" case is a mix of both. + */ + if (uom.regionMatches(true, 0, "deg", 0, 3)) { + final int length = uom.length(); + switch (length) { + case 3: return Units.DEGREE; // Exactly "deg" (ignoring case) + case 5: final char c = uom.charAt(3); + if (c != '_' && !Character.isSpaceChar(c)) break; + // else fallthrough + case 4: switch (uom.charAt(length - 1)) { + case 'K': // Unicode U+212A + case 'K': return Units.KELVIN; // Exactly "degK" (ignoring case except for 'K') + case 'C': return Units.CELSIUS; + } + } + } Map<String,Unit<?>> map = nameToUnit; if (map == null) { map = SHARED.get(locale); @@ -489,7 +510,17 @@ public class UnitFormat extends Format i } nameToUnit = map; } - return map; + /* + * The 'nameToUnit' map contains plural forms (declared in UnitAliases.properties), + * but we make a special case for "degrees", "metres" and "meters" because they + * appear in numerous places. + */ + uom = uom.replace('_', ' ').toLowerCase(locale); + uom = CharSequences.replace(CharSequences.replace(CharSequences.replace(CharSequences.toASCII(uom), + "meters", "meter"), + "metres", "metre"), + "degrees", "degree").toString(); + return map.get(uom); } /** @@ -775,18 +806,33 @@ public class UnitFormat extends Format i * Returns {@code true} if the given character is a digit in the sense of the {@code UnitFormat} parser. * Note that "digit" is taken here in a much more restrictive way than {@link Character#isDigit(int)}. */ - private static boolean isDigit(final int c) { + private static boolean isDigit(final char c) { return c >= '0' && c <= '9'; } /** * Returns {@code true} if the given character is the sign of a number according the {@code UnitFormat} parser. */ - private static boolean isSign(final int c) { + private static boolean isSign(final char c) { return c == '+' || c == '-'; } /** + * Returns {@code true} if the given character sequence contains at least one digit. + * This is a hack for allowing to recognize units like "100 feet" (in principle not + * legal, but seen in practice). This verification has some value if digits are not + * allowed as unit label or symbol. + */ + private static boolean hasDigit(final CharSequence symbol, int lower, final int upper) { + while (lower < upper) { + if (isDigit(symbol.charAt(lower++))) { + return true; + } + } + return false; + } + + /** * Parses the given text as an instance of {@code Unit}. * If the parse completes without reading the entire length of the text, an exception is thrown. * @@ -810,7 +856,7 @@ public class UnitFormat extends Format i final ParsePosition position = new ParsePosition(0); final Unit<?> unit = parse(symbols, position); final int length = symbols.length(); - final int unrecognized = CharSequences.skipTrailingWhitespaces(symbols, position.getIndex(), length); + final int unrecognized = CharSequences.skipLeadingWhitespaces(symbols, position.getIndex(), length); if (unrecognized < length) { throw new ParserException(Errors.format(Errors.Keys.UnexpectedCharactersAfter_2, CharSequences.trimWhitespaces(symbols, 0, unrecognized), @@ -845,25 +891,27 @@ public class UnitFormat extends Format i ArgumentChecks.ensureNonNull("position", position); /* * Check for authority codes (currently only EPSG, but more could be added later). - * If the unit is not an authority code (which is the most common case), then we - * will check for hard-coded unit symbols. - * - * DefinitionURI.codeOf(…) returns 'uom' directly (provided that whitespaces were already trimmed) - * if no ':' character were found, in which case the string is assumed to be the code directly. - * This is the intended behavior for AuthorityFactory, but in the particular case of this method - * we want to try to parse as a xpointer before to give up. + * Example: "urn:ogc:def:uom:EPSG::9001". If the unit is not an authority code + * (which is the most common case), only then we will parse the unit symbols. */ - int start = CharSequences.skipLeadingWhitespaces(symbols, position.getIndex(), symbols.length()); - int end = XPaths.endOfURI(symbols, start); - if (end >= 0) { - final String uom = symbols.subSequence(start, end).toString(); + int end = symbols.length(); + int start = CharSequences.skipLeadingWhitespaces(symbols, position.getIndex(), end); + int endOfURI = XPaths.endOfURI(symbols, start); + if (endOfURI >= 0) { + final String uom = symbols.subSequence(start, endOfURI).toString(); String code = DefinitionURI.codeOf("uom", Constants.EPSG, uom); - if (code != null && code != uom) { // Really identity check, see above comment. + /* + * DefinitionURI.codeOf(…) returns 'uom' directly (provided that whitespaces were already trimmed) + * if no ':' character were found, in which case the string is assumed to be the code directly. + * This is the intended behavior for AuthorityFactory, but in the particular case of this method + * we want to try to parse as a xpointer before to give up. + */ + if (code != null && code != uom) { NumberFormatException failure = null; try { final Unit<?> unit = Units.valueOfEPSG(Integer.parseInt(code)); if (unit != null) { - position.setIndex(end); + position.setIndex(endOfURI); return unit; } } catch (NumberFormatException e) { @@ -871,12 +919,28 @@ public class UnitFormat extends Format i } throw (ParserException) new ParserException(Errors.format(Errors.Keys.UnknownUnit_1, Constants.EPSG + DefaultNameSpace.DEFAULT_SEPARATOR + code), - symbols, start + Math.max(0, uom.indexOf(code))).initCause(failure); + symbols, start + Math.max(0, uom.lastIndexOf(code))).initCause(failure); } + /* + * Not an EPSG code. Maybe it is a URI like this example: + * http://schemas.opengis.net/iso/19139/20070417/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m']) + * + * If we find such 'uom' value, we could replace 'symbols' by that 'uom'. But it would cause a wrong + * error index to be reported in case of parsing failure. We will rather try to adjust the indices + * (and replace 'symbols' only in last resort). + */ code = XPaths.xpointer("uom", uom); if (code != null) { - symbols = code; - start = 0; + final int base = start; + start = endOfURI - code.length(); + do if (--start < base) { // Should never happen (see above comment), but we are paranoiac. + symbols = code; + start = 0; + break; + } while (!CharSequences.regionMatches(symbols, start, code)); + end = start + code.length(); + } else { + endOfURI = -1; } } /* @@ -887,7 +951,7 @@ public class UnitFormat extends Format i */ int operation = NOOP; // Enumeration value: IMPLICIT, MULTIPLY, DIVIDE. Unit<?> unit = null; - end = symbols.length(); + boolean hasSpaces = false; int i = start; scan: for (int n; i < end; i += n) { final int c = Character.codePointAt(symbols, i); @@ -896,7 +960,7 @@ scan: for (int n; i < end; i += n) { switch (c) { /* * For any character that are is not an operator or parenthesis, either continue the scanning of - * character or stop it, depending on whether the character is valid for a unit symbol or not. + * characters or stop it, depending on whether the character is valid for a unit symbol or not. * In the later case, we consider that we reached the end of a unit symbol. */ default: { @@ -906,7 +970,11 @@ scan: for (int n; i < end; i += n) { } continue; } - if (Character.isWhitespace(c) || Character.isDigit(c) || Characters.isSuperScript(c)) { + if (Character.isDigit(c) || Characters.isSuperScript(c)) { + continue; + } + if (Character.isSpaceChar(c)) { // NOT Character.isWhitespace(int) + hasSpaces = true; continue; } break scan; @@ -972,15 +1040,54 @@ scan: for (int n; i < end; i += n) { if (operation != IMPLICIT) { unit = apply(operation, unit, parseSymbol(symbols, start, i)); } + hasSpaces = false; operation = next; start = i + n; } /* - * At this point we either found an unrecognized character or reached the end of string. Parse the - * remaining characters as a unit and apply the pending unit operation (multiplication or division). + * At this point we either found an unrecognized character or reached the end of string. We will + * parse the remaining characters as a unit and apply the pending unit operation (multiplication + * or division). But before, we need to check if the parsing should stop at the first whitespace. + * This verification assumes that spaces are allowed only in labels specified by the label(…) + * method and in resource bundles, not in labels specified by AbstractUnit.alternate(String). */ - unit = apply(operation, unit, parseSymbol(symbols, start, i)); - position.setIndex(i); + Unit<?> component = null; + if (hasSpaces) { + end = i; + start = CharSequences.skipLeadingWhitespaces(symbols, start, i); +search: while ((i = CharSequences.skipTrailingWhitespaces(symbols, start, i)) > start) { + final String uom = symbols.subSequence(start, i).toString(); + if ((component = labelToUnit.get(uom)) != null) break; + if ((component = fromName(uom)) != null) break; + int j=i, c; + do { + c = Character.codePointBefore(symbols, j); + j -= Character.charCount(c); + if (j <= start) break search; + } while (!Character.isWhitespace(c)); + /* + * Really use Character.isWhitespace(c) above, not Character.isSpaceChar(c), because we want + * to exclude non-breaking spaces. This block should be the only place in UnitFormat class + * where we use isWhitespace(c) instead of isSpaceChar(c). + */ + i = j; // Will become the index of first space after search loop completion. + } + /* + * At this point we did not found any user-specified label or localized name matching the substring. + * Assume that the parsing should stop at the first space, on the basis that spaces are not allowed + * in unit symbols. We make an exception if we detect that the part before the first space contains + * digits (not allowed in unit symbols neither), in which case the substring may be something like + * "100 feet". + */ + if (hasDigit(symbols, start, i)) { + i = end; // Restore the full length (until the first illegal character). + } + } + if (component == null) { + component = parseSymbol(symbols, start, i); + } + unit = apply(operation, unit, component); + position.setIndex(endOfURI >= 0 ? endOfURI : i); return unit; } @@ -1017,7 +1124,6 @@ scan: for (int n; i < end; i += n) { * @return the parsed unit symbol (never {@code null}). * @throws ParserException if a problem occurred while parsing the given symbols. */ - @SuppressWarnings("fallthrough") private Unit<?> parseSymbol(final CharSequence symbols, final int lower, final int upper) throws ParserException { final String uom = CharSequences.trimWhitespaces(symbols, lower, upper).toString(); /* @@ -1109,38 +1215,10 @@ scan: for (int n; i < end; i += n) { } } /* - * Check for degrees units. Note that "deg" could be both angular and Celsius degrees. - * We try to resolve this ambiguity in the code below by looking for the "C" suffix. - * We perform a special case for those checks because the above check for unit symbol - * is case-sentive, the check for unit name (later) is case-insensitive, while this - * check for "deg" is a mix of both. - */ - if (uom.regionMatches(true, 0, "deg", 0, 3)) { - switch (length) { - case 3: return Units.DEGREE; // Exactly "deg" (ignoring case) - case 5: final char c = uom.charAt(3); - if (c != '_' && !Character.isSpaceChar(c)) break; - // else fallthrough - case 4: switch (uom.charAt(length - 1)) { - case 'K': // Unicode U+212A - case 'K': return Units.KELVIN; // Exactly "degK" (ignoring case except for 'K') - case 'C': return Units.CELSIUS; - } - } - } - /* * At this point, we have determined that the label is not a known unit symbol. * It may be a unit name, in which case the label is not case-sensitive anymore. - * The 'nameToUnit' map contains plural forms (declared in UnitAliases.properties), - * but we make a special case for "degrees", "metres" and "meters" because they - * appear in numerous places. */ - String lc = uom.replace('_', ' ').toLowerCase(locale); - lc = CharSequences.replace(CharSequences.replace(CharSequences.replace(CharSequences.toASCII(lc), - "meters", "meter"), - "metres", "metre"), - "degrees", "degree").toString(); - unit = nameToUnit().get(lc); + unit = fromName(uom); if (unit == null) { if (CharSequences.regionMatches(symbols, lower, UNITY, true)) { return Units.UNITY; Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/package-info.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/package-info.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/package-info.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/measure/package-info.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -56,7 +56,6 @@ * ({@link org.apache.sis.measure.ValueRange})</li> * <li>Formatters * ({@link org.apache.sis.measure.AngleFormat}, - * {@link org.apache.sis.measure.CoordinateFormat}, * {@link org.apache.sis.measure.RangeFormat})</li> * </ul> * Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/setup/OptionKey.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -113,6 +113,8 @@ public class OptionKey<T> implements Ser * <p>If this option is not provided, then the default value is format specific. * That default is often, but not necessarily, the {@linkplain Charset#defaultCharset() platform default}.</p> * + * @see javax.xml.bind.Marshaller#JAXB_ENCODING + * * @since 0.4 */ public static final OptionKey<Charset> ENCODING = new OptionKey<>("ENCODING", Charset.class); @@ -179,6 +181,27 @@ public class OptionKey<T> implements Ser public static final OptionKey<ByteBuffer> BYTE_BUFFER = new OptionKey<>("BYTE_BUFFER", ByteBuffer.class); /** + * The number of spaces to use for indentation when formatting text files in WKT or XML formats. + * A value of {@value org.apache.sis.io.wkt.WKTFormat#SINGLE_LINE} means to format the whole WKT + * or XML document on a single line without line feeds or indentation. + * + * <p>If this option is not provided, then the most typical default value used in Apache SIS is 2. + * Such small indentation value is used because XML documents defined by OGC standards tend to be + * verbose.</p> + * + * @see org.apache.sis.io.wkt.WKTFormat#SINGLE_LINE + * @see javax.xml.bind.Marshaller#JAXB_FORMATTED_OUTPUT + * + * @since 0.8 + */ + public static final OptionKey<Integer> INDENTATION = new OptionKey<>("INDENTATION", Integer.class); + + /* + * Note: we do not provide a LINE_SEPARATOR option for now because we can not control the line separator + * in JDK's JAXB implementation, and Apache SIS provides an org.apache.sis.io.LineAppender alternative. + */ + + /** * The name of this key. For {@code OptionKey} instances, it shall be the name of the static constants. * For subclasses of {@code OptionKey}, there is no restriction. */ Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/Version.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/Version.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/Version.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/Version.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -70,7 +70,9 @@ public class Version implements CharSequ */ private static final Version[] CONSTANTS = { new Version("1"), - new Version("2") + new Version("2"), + new Version("1.0"), + new Version("1.1") }; /** Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/collection/TreeTableFormat.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -414,8 +414,10 @@ public class TreeTableFormat extends Tab parseValue(node, columns[ci], formats[ci], text.subSequence(indexOfValue, endOfValue).toString()); } if (!found) break; - // The end of this column will be the beginning of the next column, - // after skipping the last character of the column separator. + /* + * The end of this column will be the beginning of the next column, + * after skipping the last character of the column separator. + */ indexOfValue = matcher.end(); } } catch (ParseException e) { Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -378,7 +378,7 @@ public final class Errors extends Indexe public static final short IllegalUnicodeCodePoint_2 = 61; /** - * Can not use the “{1}” format with “{0}”. + * Can not use the {1} format with “{0}”. */ public static final short IncompatibleFormat_2 = 62; Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties [ISO-8859-1] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties [ISO-8859-1] Fri Feb 3 08:01:02 2017 @@ -86,7 +86,7 @@ IllegalPropertyValueClass_2 = Prop IllegalPropertyValueClass_3 = Expected an instance of \u2018{1}\u2019 for the \u201c{0}\u201d property, but got an instance of \u2018{2}\u2019. IllegalRange_2 = Range [{0} \u2026 {1}] is not valid. IllegalUnicodeCodePoint_2 = Value {1} for \u201c{0}\u201d is not a valid Unicode code point. -IncompatibleFormat_2 = Can not use the \u201c{1}\u201d format with \u201c{0}\u201d. +IncompatibleFormat_2 = Can not use the {1} format with \u201c{0}\u201d. IncompatiblePropertyValue_1 = Property \u201c{0}\u201d has an incompatible value. IncompatibleUnit_1 = Unit \u201c{0}\u201d is incompatible with current value. IncompatibleUnits_2 = Units \u201c{0}\u201d and \u201c{1}\u201d are incompatible. Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties [ISO-8859-1] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties [ISO-8859-1] Fri Feb 3 08:01:02 2017 @@ -83,7 +83,7 @@ IllegalPropertyValueClass_2 = La p IllegalPropertyValueClass_3 = Une instance \u2018{1}\u2019 \u00e9tait attendue pour la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb, mais la valeur donn\u00e9e est une instance de \u2018{2}\u2019. IllegalRange_2 = La plage [{0} \u2026 {1}] n\u2019est pas valide. IllegalUnicodeCodePoint_2 = La valeur {1} de \u00ab\u202f{0}\u202f\u00bb n\u2019est pas un code Unicode valide. -IncompatibleFormat_2 = Le format \u00ab\u202f{1}\u202f\u00bb ne s\u2019applique pas \u00e0 \u00ab\u202f{0}\u202f\u00bb. +IncompatibleFormat_2 = Le format {1} ne s\u2019applique pas \u00e0 \u00ab\u202f{0}\u202f\u00bb. IncompatiblePropertyValue_1 = La valeur de la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb n\u2019est pas compatible. IncompatibleUnit_1 = L\u2019unit\u00e9 \u00ab\u202f{0}\u202f\u00bb n\u2019est pas compatible avec la valeur actuelle. IncompatibleUnits_2 = Les unit\u00e9s \u00ab\u202f{0}\u202f\u00bb et \u00ab\u202f{1}\u202f\u00bb ne sont pas compatibles. Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/MarshallerPool.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/MarshallerPool.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/MarshallerPool.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/MarshallerPool.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -32,7 +32,9 @@ import org.apache.sis.internal.system.De import org.apache.sis.internal.system.DefaultFactories; import org.apache.sis.internal.jaxb.AdapterReplacement; import org.apache.sis.internal.jaxb.TypeRegistration; +import org.apache.sis.internal.util.Constants; import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.CharSequences; /** @@ -66,11 +68,6 @@ import org.apache.sis.util.ArgumentCheck */ public class MarshallerPool { /** - * The indentation string, fixed to 2 spaces instead of 4 because ISO/OGC XML are very verbose. - */ - private static final String INDENTATION = " "; - - /** * Amount of nanoseconds to wait before to remove unused (un)marshallers. * This is a very approximative value: actual timeout will not be shorter, * but may be twice longer. @@ -78,21 +75,19 @@ public class MarshallerPool { private static final long TIMEOUT = 15000000000L; // 15 seconds. /** - * Kind of JAXB implementations. - */ - private static final byte INTERNAL = 0, ENDORSED = 1, OTHER = 2; - - /** * The JAXB context to use for creating marshaller and unmarshaller. + * + * @see #createMarshaller() + * @see #createUnmarshaller() */ - private final JAXBContext context; + protected final JAXBContext context; /** - * {@link #INTERNAL} if the JAXB implementation is the one bundled in the JDK, - * {@link #ENDORSED} if the TAXB implementation is the endorsed JAXB (Glassfish), or - * {@link #OTHER} if unknown. + * {@code INTERNAL} if the JAXB implementation is the one bundled in the JDK, + * {@code ENDORSED} if the TAXB implementation is the endorsed JAXB (Glassfish), or + * {@code null} if unknown. */ - private final byte implementation; + private final Implementation implementation; /** * The mapper between namespaces and prefix. @@ -164,7 +159,12 @@ public class MarshallerPool { * @throws JAXBException if the JAXB context can not be created. */ public MarshallerPool(final Map<String,?> properties) throws JAXBException { - this(TypeRegistration.getSharedContext(), properties); + /* + * We currently add the default root adapters only when using the JAXB context provided by Apache SIS. + * We presume that if the user specified his own JAXBContext, then he does not expect us to change the + * classes that he wants to marshal. + */ + this(TypeRegistration.getSharedContext(), TypeRegistration.addDefaultRootAdapters(properties)); } /** @@ -186,30 +186,12 @@ public class MarshallerPool { ArgumentChecks.ensureNonNull("context", context); this.context = context; replacements = DefaultFactories.createServiceLoader(AdapterReplacement.class); - /* - * Detects if we are using the endorsed JAXB implementation (i.e. the one provided in - * separated JAR files) or the one bundled in JDK 6. We use the JAXB context package - * name as a criterion: - * - * JAXB endorsed JAR uses "com.sun.xml.bind" - * JAXB bundled in JDK uses "com.sun.xml.internal.bind" - */ - String classname = context.getClass().getName(); - if (classname.startsWith("com.sun.xml.internal.bind.")) { - classname = "org.apache.sis.xml.OGCNamespacePrefixMapper"; - implementation = INTERNAL; - } else if (classname.startsWith(Pooled.ENDORSED_PREFIX)) { - classname = "org.apache.sis.xml.OGCNamespacePrefixMapper_Endorsed"; - implementation = ENDORSED; - } else { - classname = null; - implementation = OTHER; - } + implementation = Implementation.detect(context); /* * Prepares a copy of the property map (if any), then removes the * properties which are handled especially by this constructor. */ - template = new PooledTemplate(properties, implementation == INTERNAL); + template = new PooledTemplate(properties, implementation); final Object rootNamespace = template.remove(XML.DEFAULT_NAMESPACE, ""); /* * Instantiates the OGCNamespacePrefixMapper appropriate for the implementation @@ -217,6 +199,7 @@ public class MarshallerPool { * usual ClassNotFoundException if the class was found but its parent class has * not been found. */ + final String classname = implementation.mapper; if (classname == null) { mapper = null; } else try { @@ -243,8 +226,10 @@ public class MarshallerPool { try { ((Pooled) marshaller).reset(template); } catch (JAXBException exception) { - // Not expected to happen because we are supposed - // to reset the properties to their initial values. + /* + * Not expected to happen because we are supposed + * to reset the properties to their initial values. + */ Logging.unexpectedException(Logging.getLogger(Loggers.XML), MarshallerPool.class, "recycle", exception); return; } @@ -438,23 +423,23 @@ public class MarshallerPool { * * @return a new marshaller configured for formatting OGC/ISO XML. * @throws JAXBException if an error occurred while creating and configuring the marshaller. + * + * @see #context + * @see #acquireMarshaller() */ protected Marshaller createMarshaller() throws JAXBException { final Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); - marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8"); - switch (implementation) { - case INTERNAL: { - marshaller.setProperty("com.sun.xml.internal.bind.namespacePrefixMapper", mapper); - marshaller.setProperty("com.sun.xml.internal.bind.indentString", INDENTATION); - break; - } - case ENDORSED: { - marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", mapper); - marshaller.setProperty("com.sun.xml.bind.indentString", INDENTATION); - break; - } - // Do nothing for the OTHER case. + /* + * Note: we do not set the Marshaller.JAXB_ENCODING property because specification + * said that the default value is "UTF-8", which is what we want. + */ + String key; + if ((key = implementation.mapperKey) != null) { + marshaller.setProperty(key, mapper); + } + if ((key = implementation.indentKey) != null) { + marshaller.setProperty(key, CharSequences.spaces(Constants.DEFAULT_INDENTATION)); } synchronized (replacements) { for (final AdapterReplacement adapter : replacements) { @@ -471,6 +456,9 @@ public class MarshallerPool { * * @return a new unmarshaller configured for parsing OGC/ISO XML. * @throws JAXBException if an error occurred while creating and configuring the unmarshaller. + * + * @see #context + * @see #acquireUnmarshaller() */ protected Unmarshaller createUnmarshaller() throws JAXBException { final Unmarshaller unmarshaller = context.createUnmarshaller(); Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/Pooled.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/Pooled.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/Pooled.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/Pooled.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -38,6 +38,7 @@ import org.apache.sis.util.logging.Warni import org.apache.sis.internal.util.CollectionsExt; import org.apache.sis.internal.jaxb.Context; import org.apache.sis.internal.jaxb.LegacyNamespaces; +import org.apache.sis.internal.jaxb.TypeRegistration; /** @@ -49,7 +50,7 @@ import org.apache.sis.internal.jaxb.Lega * * @author Martin Desruisseaux (Geomatys) * @since 0.3 - * @version 0.3 + * @version 0.8 * @module */ abstract class Pooled { @@ -60,21 +61,12 @@ abstract class Pooled { private static final String[] SCHEMA_KEYS = {"gmd"}; /** - * The prefix of property names which are provided in external (endorsed) implementation of JAXB. - * This is slightly different than the prefix used by the implementation bundled with the JDK 6, - * which is {@code "com.sun.xml.internal.bind"}. - * - * @see #convertPropertyKey(String) - */ - static final String ENDORSED_PREFIX = "com.sun.xml.bind."; - - /** * {@code true} if the JAXB implementation is the one bundled in JDK 6, or {@code false} * if this is the external implementation provided as a JAR file in the endorsed directory. * If {@code true}, then an additional {@code "internal"} package name needs to be inserted * in the property keys. * - * @see #convertPropertyKey(String) + * @see Implementation#toInternal(String) */ private final boolean internal; @@ -156,6 +148,14 @@ abstract class Pooled { private ValueConverter converter; /** + * Converters from arbitrary classes implementing GeoAPI interfaces to Apache SIS implementations + * providing JAXB annotations, or null or an empty array if none. This is used at marshalling time. + * + * @see #getRootAdapters() + */ + private TypeRegistration[] rootAdapters; + + /** * The object to inform about warnings, or {@code null} if none. */ private WarningListener<?> warningListener; @@ -170,7 +170,7 @@ abstract class Pooled { /** * Creates a {@link PooledTemplate}. * - * @param internal {@code true} if the JAXB implementation is the one bundled in JDK 6, + * @param internal {@code true} if the JAXB implementation is the one bundled in JDK 6, * or {@code false} if this is the external implementation provided as a JAR file * in the endorsed directory. */ @@ -199,7 +199,7 @@ abstract class Pooled { * @throws JAXBException if an error occurred while setting a property. */ final void initialize(final Pooled template) throws JAXBException { - reset(template); // Set the SIS properties first. JAXB properties are set below. + reset(template); // Set the SIS properties first. JAXB properties are set below. for (final Map.Entry<Object,Object> entry : template.initialProperties.entrySet()) { setStandardProperty((String) entry.getKey(), entry.getValue()); } @@ -226,6 +226,7 @@ abstract class Pooled { versionGML = template.versionGML; resolver = template.resolver; converter = template.converter; + rootAdapters = template.rootAdapters; warningListener = template.warningListener; resetTime = System.nanoTime(); if (this instanceof Marshaller) { @@ -303,22 +304,6 @@ abstract class Pooled { } /** - * Converts a property key from the JAXB name to the underlying implementation name. - * This applies only to property keys in the {@code "com.sun.xml.bind"} namespace. - * - * @param key the JAXB property key. - * @return the property key to use. - */ - private String convertPropertyKey(String key) { - if (internal && key.startsWith(ENDORSED_PREFIX)) { - final StringBuilder buffer = new StringBuilder(key.length() + 10); - key = buffer.append("com.sun.xml.internal.bind.") - .append(key, ENDORSED_PREFIX.length(), key.length()).toString(); - } - return key; - } - - /** * A method which is common to both {@code Marshaller} and {@code Unmarshaller}. * It saves the initial state if it was not already done, but subclasses will * need to complete the work. @@ -397,6 +382,11 @@ abstract class Pooled { } return; } + case TypeRegistration.ROOT_ADAPTERS: { + rootAdapters = (TypeRegistration[]) value; + // No clone for now because ROOT_ADAPTERS is not yet a public API. + return; + } } } catch (ClassCastException | IllformedLocaleException e) { throw new PropertyException(Errors.format( @@ -406,7 +396,9 @@ abstract class Pooled { * If we reach this point, the given name is not a SIS property. Try to handle * it as a (un)marshaller-specific property, after saving the previous value. */ - name = convertPropertyKey(name); + if (internal) { + name = Implementation.toInternal(name); + } if (!initialProperties.containsKey(name)) { if (initialProperties.put(name, getStandardProperty(name)) != null) { // Should never happen, unless on concurrent changes in a backgroung thread. @@ -420,7 +412,7 @@ abstract class Pooled { * A method which is common to both {@code Marshaller} and {@code Unmarshaller}. */ @SuppressWarnings("ReturnOfCollectionOrArrayField") // Because unmodifiable. - public final Object getProperty(final String name) throws PropertyException { + public final Object getProperty(String name) throws PropertyException { switch (name) { case XML.LOCALE: return locale; case XML.TIMEZONE: return timezone; @@ -445,8 +437,12 @@ abstract class Pooled { default: return null; } } + case TypeRegistration.ROOT_ADAPTERS: return (rootAdapters != null) ? rootAdapters.clone() : null; default: { - return getStandardProperty(convertPropertyKey(name)); + if (internal) { + name = Implementation.toInternal(name); + } + return getStandardProperty(name); } } } @@ -493,6 +489,18 @@ abstract class Pooled { public abstract <A extends XmlAdapter> A getAdapter(final Class<A> type); /** + * Returns the adapters to apply on the root object to marshal, or {@code null} or an empty array if none. + * This is used for converting from arbitrary implementations of GeoAPI interfaces to Apache SIS implementations + * providing JAXB annotations. + * + * @return a direct reference to the internal array of converters - do not modify. + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + final TypeRegistration[] getRootAdapters() { + return rootAdapters; + } + + /** * A method which is common to both {@code Marshaller} and {@code Unmarshaller}. * It saves the initial state if it was not already done, but subclasses will * need to complete the work. Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/PooledMarshaller.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/PooledMarshaller.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/PooledMarshaller.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/PooledMarshaller.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -36,6 +36,7 @@ import javax.xml.validation.Schema; import org.xml.sax.ContentHandler; import org.w3c.dom.Node; import org.apache.sis.internal.jaxb.Context; +import org.apache.sis.internal.jaxb.TypeRegistration; /** @@ -53,7 +54,7 @@ import org.apache.sis.internal.jaxb.Cont * * @author Martin Desruisseaux (Geomatys) * @since 0.3 - * @version 0.4 + * @version 0.8 * @module */ final class PooledMarshaller extends Pooled implements Marshaller { @@ -90,7 +91,7 @@ final class PooledMarshaller extends Poo if (key instanceof String) { final String k = (String) key; if (value == null && (k.endsWith(".xmlHeaders") || k.equals(JAXB_SCHEMA_LOCATION))) { - value = ""; // Null value doesn't seem to be accepted for those properties. + value = ""; // Null value does not seem to be accepted for those properties. } marshaller.setProperty(k, value); } else if (key == AttachmentMarshaller.class) { @@ -114,14 +115,30 @@ final class PooledMarshaller extends Poo } /** + * Converts the given arbitrary object to an implementation having JAXB annotations. + * If the given object is not recognized or is already an instance of the expected class, + * then it is returned unchanged. + */ + private Object toImplementation(final Object value) throws JAXBException { + final TypeRegistration[] converters = getRootAdapters(); + if (converters != null) { + for (final TypeRegistration t : converters) { + final Object c = t.toImplementation(value); + if (c != null) return c; + } + } + return value; + } + + /** * Marshals to the given output with on-the-fly substitution of namespaces. * This method is invoked only when the user asked to marshal to a different GML version * than the one supported natively by SIS, i.e. when {@link #getFilterVersion()} returns * a non-null value. * - * @param object the object to marshall. - * @param output the writer created by SIS (<b>not</b> the writer given by the user). - * @param version Identify the namespace substitutions to perform. + * @param object the object to marshall. + * @param output the writer created by SIS (<b>not</b> the writer given by the user). + * @param version identifies the namespace substitutions to perform. */ private void marshal(final Object object, XMLStreamWriter output, final FilterVersion version) throws XMLStreamException, JAXBException @@ -129,7 +146,7 @@ final class PooledMarshaller extends Poo output = new FilteredStreamWriter(output, version); final Context context = begin(); try { - marshaller.marshal(object, output); + marshaller.marshal(toImplementation(object), output); } finally { context.finish(); } @@ -150,7 +167,7 @@ final class PooledMarshaller extends Poo // Marshalling to the default GML version. final Context context = begin(); try { - marshaller.marshal(object, output); + marshaller.marshal(toImplementation(object), output); } finally { context.finish(); } @@ -171,7 +188,7 @@ final class PooledMarshaller extends Poo // Marshalling to the default GML version. final Context context = begin(); try { - marshaller.marshal(object, output); + marshaller.marshal(toImplementation(object), output); } finally { context.finish(); } @@ -194,7 +211,7 @@ final class PooledMarshaller extends Poo // Marshalling to the default GML version. final Context context = begin(); try { - marshaller.marshal(object, output); + marshaller.marshal(toImplementation(object), output); } finally { context.finish(); } @@ -215,7 +232,7 @@ final class PooledMarshaller extends Poo // Marshalling to the default GML version. final Context context = begin(); try { - marshaller.marshal(object, output); + marshaller.marshal(toImplementation(object), output); } finally { context.finish(); } @@ -236,7 +253,7 @@ final class PooledMarshaller extends Poo // Marshalling to the default GML version. final Context context = begin(); try { - marshaller.marshal(object, output); + marshaller.marshal(toImplementation(object), output); } finally { context.finish(); } @@ -257,7 +274,7 @@ final class PooledMarshaller extends Poo // Marshalling to the default GML version. final Context context = begin(); try { - marshaller.marshal(object, output); + marshaller.marshal(toImplementation(object), output); } finally { context.finish(); } @@ -275,7 +292,7 @@ final class PooledMarshaller extends Poo } final Context context = begin(); try { - marshaller.marshal(object, output); + marshaller.marshal(toImplementation(object), output); } finally { context.finish(); } @@ -295,7 +312,7 @@ final class PooledMarshaller extends Poo // Marshalling to the default GML version. final Context context = begin(); try { - marshaller.marshal(object, output); + marshaller.marshal(toImplementation(object), output); } finally { context.finish(); } @@ -314,7 +331,7 @@ final class PooledMarshaller extends Poo } else { final Context context = begin(); try { - return marshaller.getNode(object); + return marshaller.getNode(toImplementation(object)); } finally { context.finish(); } Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/PooledTemplate.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/PooledTemplate.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/PooledTemplate.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/PooledTemplate.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -40,15 +40,18 @@ final class PooledTemplate extends Poole * Creates a new template. * * @param properties the properties to be given to JAXB (un)marshallers, or {@code null} if none. - * @param internal {@code true} if the JAXB implementation is the one bundled in JDK 6, - * or {@code false} if this is the external implementation provided as a JAR file - * in the endorsed directory. + * @param internal {@code true} if the JAXB implementation is the one bundled in JDK 6, or + * {@code false} if this is the external implementation provided as a JAR file + * in the endorsed directory. */ - PooledTemplate(final Map<String,?> properties, final boolean internal) throws PropertyException { - super(internal); + PooledTemplate(final Map<String,?> properties, final Implementation implementation) throws PropertyException { + super(implementation == Implementation.INTERNAL); if (properties != null) { for (final Map.Entry<String,?> entry : properties.entrySet()) { - setProperty(entry.getKey(), entry.getValue()); + final String key = entry.getKey(); + if (implementation.filterProperty(key)) { + setProperty(key, entry.getValue()); + } } } } @@ -83,7 +86,7 @@ final class PooledTemplate extends Poole * <p>Current implementation expects values of type {@code String}, but this may be generalized * in a future SIS version if there is a need for that.</p> * - * @param name the name of the property to remove. + * @param name the name of the property to remove. * @param defaultValue the default value to return if the given property is not defined in the map. * @return the old value of that property, or {@code defaultValue} if the given property was not defined. * @throws PropertyException if the given property is not of the expected type. Modified: sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/XML.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/XML.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/XML.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/main/java/org/apache/sis/xml/XML.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -102,6 +102,7 @@ public final class XML extends Static { * {@link org.apache.sis.metadata.iso.DefaultMetadata#setLanguage(Locale) setLanguage(Locale)} * method will have precedence over this property. This behavior is compliant with INSPIRE rules. * + * @see org.apache.sis.setup.OptionKey#LOCALE * @see Marshaller#setProperty(String, Object) * @see org.apache.sis.metadata.iso.DefaultMetadata#setLanguage(Locale) */ @@ -115,6 +116,8 @@ public final class XML extends Static { * <div class="section">Default behavior</div> * If this property is never set, then (un)marshalling will use the * {@linkplain TimeZone#getDefault() default timezone}. + * + * @see org.apache.sis.setup.OptionKey#TIMEZONE */ public static final String TIMEZONE = "org.apache.sis.xml.timezone"; @@ -308,8 +311,7 @@ public final class XML extends Static { * this field is initially null, then created by {@link #getPool()} when first needed. * Once created the field value usually doesn't change. However the field may be reset * to {@code null} in an OSGi context when modules are loaded or unloaded, because the - * set of classes returned by {@link TypeRegistration#defaultClassesToBeBound()} may - * have changed. + * set of classes returned by {@link TypeRegistration#load(boolean)} may have changed. * * @see #getPool() */ Modified: sis/trunk/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPathsTest.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPathsTest.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPathsTest.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/test/java/org/apache/sis/internal/util/XPathsTest.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -16,6 +16,7 @@ */ package org.apache.sis.internal.util; +import org.apache.sis.util.Characters; import org.apache.sis.test.TestCase; import org.junit.Test; @@ -42,6 +43,7 @@ public final strictfp class XPathsTest e assertEquals(97, XPaths.endOfURI("http://schemas.opengis.net/iso/19139/20070417/resources/uom/gmxUom.xml#xpointer(//*[@gml:id='m'])", 0)); assertEquals(-1, XPaths.endOfURI("m/s", 0)); assertEquals(-1, XPaths.endOfURI("m.s", 0)); + assertEquals(11, XPaths.endOfURI("EPSG" + Characters.NO_BREAK_SPACE + ": 9001", 0)); } /** Modified: sis/trunk/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java URL: http://svn.apache.org/viewvc/sis/trunk/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java?rev=1781503&r1=1781502&r2=1781503&view=diff ============================================================================== --- sis/trunk/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java [UTF-8] (original) +++ sis/trunk/core/sis-utility/src/test/java/org/apache/sis/measure/UnitFormatTest.java [UTF-8] Fri Feb 3 08:01:02 2017 @@ -19,6 +19,7 @@ package org.apache.sis.measure; import java.util.Set; import java.util.HashSet; import java.util.Locale; +import java.text.ParsePosition; import java.lang.reflect.Field; import javax.measure.Unit; import javax.measure.format.ParserException; @@ -281,6 +282,7 @@ public final strictfp class UnitFormatTe fail("Should not accept unknown unit."); } catch (ParserException e) { final String message = e.getMessage(); + assertTrue(message, message.contains("degree")); assertTrue(message, message.contains("foo")); } // Tests with localisation. @@ -289,7 +291,8 @@ public final strictfp class UnitFormatTe fail("Should not accept localized unit unless requested."); } catch (ParserException e) { final String message = e.getMessage(); - assertTrue(message, message.contains("mètre cube")); + assertTrue(message, message.contains("mètre")); + assertTrue(message, message.contains("cube")); } f.setLocale(Locale.FRANCE); assertSame(Units.CUBIC_METRE, f.parse("mètre cube")); @@ -395,6 +398,29 @@ public final strictfp class UnitFormatTe } /** + * Tests parsing a unit from another position than zero and verifies that {@code UnitFormat} detects + * correctly where the unit symbol ends. + */ + @Test + @DependsOnMethod("testParseSymbol") + public void testParsePosition() { + final UnitFormat f = new UnitFormat(Locale.UK); + final ParsePosition pos = new ParsePosition(4); + assertSame(Units.CENTIMETRE, f.parse("ABC cm DEF", pos)); + assertEquals("ParsePosition.getIndex()", 6, pos.getIndex()); + assertEquals("ParsePosition.getErrorIndex()", -1, pos.getErrorIndex()); + /* + * Adding "cm DEF" as a unit label should allow UnitFormat to recognize those characters. + * We associate a random unit to that label, just for testing purpose. + */ + pos.setIndex(4); + f.label(Units.HECTARE, "cm DEF"); + assertSame(Units.HECTARE, f.parse("ABC cm DEF", pos)); + assertEquals("ParsePosition.getIndex()", 10, pos.getIndex()); + assertEquals("ParsePosition.getErrorIndex()", -1, pos.getErrorIndex()); + } + + /** * Tests {@link UnitFormat#clone()}. */ @Test
