This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new b890c8c Refactor the way we store WKT trees in a dictionary. Instead
of having `Element` working in two modes (mutable or immutable), keep `Element`
always mutable and create immutable snapshots with a separated class,
`StoredTree`.
b890c8c is described below
commit b890c8c982630de77f77f9a886713f950141fef0
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Wed Nov 18 00:36:47 2020 +0100
Refactor the way we store WKT trees in a dictionary.
Instead of having `Element` working in two modes (mutable or immutable),
keep `Element` always mutable and create immutable snapshots with a
separated class, `StoredTree`.
---
.../java/org/apache/sis/io/wkt/AbstractParser.java | 205 +++++----
.../main/java/org/apache/sis/io/wkt/Element.java | 287 ++++++------
.../apache/sis/io/wkt/GeodeticObjectParser.java | 12 +-
.../org/apache/sis/io/wkt/MathTransformParser.java | 4 +-
.../java/org/apache/sis/io/wkt/StoredTree.java | 483 +++++++++++++++++++++
.../java/org/apache/sis/io/wkt/WKTDictionary.java | 116 +++--
.../main/java/org/apache/sis/io/wkt/WKTFormat.java | 65 +--
.../java/org/apache/sis/io/wkt/doc-files/ESRI.txt | 6 +-
.../referencing/factory/GeodeticObjectFactory.java | 1 +
.../transform/DefaultMathTransformFactory.java | 1 +
.../java/org/apache/sis/io/wkt/ElementTest.java | 31 +-
.../sis/io/wkt/GeodeticObjectParserTest.java | 4 +-
.../apache/sis/io/wkt/MathTransformParserTest.java | 4 +-
.../org/apache/sis/io/wkt/WKTDictionaryTest.java | 78 +++-
.../resources/org/apache/sis/io/wkt/ExtraCRS.txt | 44 +-
15 files changed, 922 insertions(+), 419 deletions(-)
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/AbstractParser.java
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/AbstractParser.java
index b255587..09d9e77 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/AbstractParser.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/AbstractParser.java
@@ -37,7 +37,6 @@ import org.apache.sis.internal.system.Loggers;
import org.apache.sis.internal.util.StandardDateFormat;
import org.apache.sis.measure.Units;
import org.apache.sis.measure.UnitFormat;
-import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Errors;
@@ -53,11 +52,11 @@ import static
org.apache.sis.util.ArgumentChecks.ensureNonNull;
* <p>In current version, parsers are not intended to be subclassed outside
this package.</p>
*
* <p>Parsers are not synchronized. It is recommended to create separate
parser instances for each thread.
- * If multiple threads access a parser concurrently, it must be synchronized
externally.</p>
+ * If many threads access the same parser instance concurrently, it must be
synchronized externally.</p>
*
* @author Rémi Eve (IRD)
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.1
* @since 0.6
* @module
*/
@@ -71,19 +70,21 @@ abstract class AbstractParser implements Parser {
/**
* A mode for the {@link Element#pullElement(int, String...)} method
meaning that the requested element
- * is optional but is not necessarily first. If no element have a name
matching one of the requested names,
+ * is optional but not necessarily first. If no element has a name
matching one of the requested names,
* then {@code pullElement(…)} returns {@code null}.
*/
static final int OPTIONAL = 1;
/**
* A mode for the {@link Element#pullElement(int, String...)} method
meaning that an exception shall be
- * thrown if no element have a name matching one of the requested names.
+ * thrown if no element has a name matching one of the requested names.
*/
static final int MANDATORY = 2;
/**
- * The locale for error messages (not for number parsing), or {@code null}
for the system default.
+ * The locale for formatting error messages if parsing fails, or {@code
null} for system default.
+ * This is <strong>not</strong> the locale for parsing number or date
values.
+ * The locale for numbers and dates is contained in {@link #symbols}.
*/
final Locale errorLocale;
@@ -115,20 +116,25 @@ abstract class AbstractParser implements Parser {
/**
* Reference to the {@link WKTFormat#fragments} map, or an empty map if
none.
- * This parser will only read this map, never write to it.
+ * Shall be used in read-only mode; never write through this reference.
+ *
+ * @see WKTFormat#addFragment(String, StoredTree)
*/
- final Map<String,Element> fragments;
+ final Map<String,StoredTree> fragments;
/**
* Keyword of unknown elements. The ISO 19162 specification requires that
we ignore unknown elements,
- * but we will nevertheless report them as warnings.
- * The meaning of this map is:
+ * but we will nevertheless report them as {@linkplain #warnings}. The
meaning of this map is:
+ *
* <ul>
* <li><b>Keys</b>: keyword of ignored elements. Note that a key may be
null.</li>
* <li><b>Values</b>: keywords of all elements containing an element
identified by the above-cited key.
* This list is used for helping the users to locate the ignored
elements.</li>
* </ul>
*
+ * Content of this map is not discarded immediately {@linkplain
#getAndClearWarnings(Object) after parsing}.
+ * It is kept for some time because {@link Warnings} will copy its content
only when first needed.
+ *
* @see #getAndClearWarnings(Object)
*/
final Map<String, List<String>> ignoredElements;
@@ -136,6 +142,7 @@ abstract class AbstractParser implements Parser {
/**
* The warning (other than {@link #ignoredElements}) that occurred during
the parsing.
* Created when first needed and reset to {@code null} when a new parsing
start.
+ * Warnings are reported when {@link #getAndClearWarnings(Object)} is
invoked.
*/
private Warnings warnings;
@@ -149,8 +156,8 @@ abstract class AbstractParser implements Parser {
* @param unitFormat the unit format provided by {@link WKTFormat}, or
{@code null} for a default format.
* @param errorLocale the locale for error messages (not for parsing),
or {@code null} for the system default.
*/
- AbstractParser(final Symbols symbols, final Map<String,Element> fragments,
NumberFormat numberFormat,
- final DateFormat dateFormat, final UnitFormat unitFormat, final
Locale errorLocale)
+ AbstractParser(final Symbols symbols, final Map<String,StoredTree>
fragments, NumberFormat numberFormat,
+ final DateFormat dateFormat, final UnitFormat unitFormat,
final Locale errorLocale)
{
ensureNonNull("symbols", symbols);
if (numberFormat == null) {
@@ -184,23 +191,44 @@ abstract class AbstractParser implements Parser {
/**
* Returns the name of the class providing the publicly-accessible {@code
createFromWKT(String)} method.
- * This information is used for logging purpose only.
+ * This information is used for logging purposes only. Values can be:
+ *
+ * <ul>
+ * <li>{@code "org.apache.sis.io.wkt.WKTFormat"}</li>
+ * <li>{@code
"org.apache.sis.referencing.factory.GeodeticObjectFactory"}</li>
+ * <li>{@code
"org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory"}</li>
+ * </ul>
*/
abstract String getPublicFacade();
/**
* Returns the name of the method invoked from {@link #getPublicFacade()}.
- * This information is used for logging purpose only.
+ * This information is used for logging purposes only.
+ * Another possible value is {@codd "parse"}.
*/
String getFacadeMethod() {
return "createFromWKT";
}
/**
- * Creates the object from a string and log the warnings if any.
- * This method is for implementation of {@code createFromWKT(String)}
method is SIS factories only.
+ * Logs the given record for a warning that occurred during parsing.
+ * This is used when we can not use the {@link #warning warning methods},
+ * or when the information is not worth to report as a warning.
+ */
+ final void log(final LogRecord record) {
+ Logger logger = Logging.getLogger(Loggers.WKT);
+ record.setSourceClassName (getPublicFacade());
+ record.setSourceMethodName(getFacadeMethod());
+ record.setLoggerName(logger.getName());
+ logger.log(record);
+ }
+
+ /**
+ * Creates the object from a WKT string and logs the warnings if any.
+ * This method is for implementation of {@code createFromWKT(String)}
method in SIS factories only.
+ * Callers should ensure that {@code wkt} is non-null and non-empty (this
method does not verify).
*
- * @param text coordinate system encoded in Well-Known Text format
(version 1 or 2).
+ * @param wkt object encoded in Well-Known Text format (version 1 or 2).
* @return the result of parsing the given text.
* @throws FactoryException if the object creation failed.
*
@@ -208,34 +236,39 @@ abstract class AbstractParser implements Parser {
* @see
org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory#createFromWKT(String)
*/
@Override
- public final Object createFromWKT(final String text) throws
FactoryException {
- final Object value;
+ public final Object createFromWKT(final String wkt) throws
FactoryException {
+ Object result = null;
+ Warnings warnings;
try {
- value = parseObject(text, new ParsePosition(0));
+ result = createFromWKT(wkt, new ParsePosition(0));
} catch (ParseException exception) {
final Throwable cause = exception.getCause();
if (cause instanceof FactoryException) {
throw (FactoryException) cause;
}
throw new FactoryException(exception.getLocalizedMessage(),
exception);
+ } finally {
+ warnings = getAndClearWarnings(result);
}
- final Warnings warnings = getAndClearWarnings(value);
if (warnings != null) {
log(new LogRecord(Level.WARNING, warnings.toString()));
}
- return value;
+ return result;
}
/**
- * Logs the given record. This is used only when we can not use the {@link
#warning warning methods},
- * or when the information is not worth to report as a warning.
+ * Parses a <cite>Well-Know Text</cite> from specified position as a
geodetic object.
+ * Caller should invoke {@link #getAndClearWarnings(Object)} in a {@code
finally} block
+ * after this method.
+ *
+ * @return the parsed object.
+ * @throws ParseException if the string can not be parsed.
*/
- final void log(final LogRecord record) {
- Logger logger = Logging.getLogger(Loggers.WKT);
- record.setSourceClassName(getPublicFacade());
- record.setSourceMethodName(getFacadeMethod());
- record.setLoggerName(logger.getName());
- logger.log(record);
+ Object createFromWKT(final String text, final ParsePosition position)
throws ParseException {
+ final Element root = new Element(textToTree(text, position));
+ final Object result = buildFromTree(root);
+ root.close(ignoredElements);
+ return result;
}
/**
@@ -243,92 +276,47 @@ abstract class AbstractParser implements Parser {
* Current implementation assumes that the fragment name is a Unicode
identifier,
* except for the first character which is not required to be an
identifier start.
*/
- static int endOfFragmentName(final String text, int upper) {
+ static int endOfFragmentName(final String text, int position) {
final int length = text.length();
- while (upper < length) {
- final int c = text.codePointAt(upper);
+ while (position < length) {
+ final int c = text.codePointAt(position);
if (!Character.isUnicodeIdentifierPart(c)) break;
- upper += Character.charCount(c);
+ position += Character.charCount(c);
}
- return upper;
+ return position;
}
/**
- * Parses a <cite>Well Know Text</cite> (WKT) as a tree of {@link
Element}s.
+ * Parses the <cite>Well Know Text</cite> from specified position as a
tree of {@link Element}s.
* This tree can be given to {@link #buildFromTree(Element)} for producing
a geodetic object.
*
- * @param text the text to be parsed.
- * @param position the position to start parsing from.
- * @param sharedValues non-null if parsing a WKT tree to be kept for a
long time.
- * In such case, contains values found during
parsing of other elements.
+ * @param wkt the Well-Known Text to be parsed.
+ * @param position before parsing, provides index of the first character
to parse in the {@code wkt} string.
+ * After parsing completion, provides index after the
last character parsed.
* @return the parsed object as a tree of {@link Element}s.
* @throws ParseException if the string can not be parsed.
*
* @see WKTFormat#textToTree(String, ParsePosition)
*/
- final Element textToTree(final String text, final ParsePosition position,
final Map<Object,Object> sharedValues)
- throws ParseException
- {
+ final Element textToTree(final String wkt, final ParsePosition position)
throws ParseException {
+ int lower = CharSequences.skipLeadingWhitespaces(wkt,
position.getIndex(), wkt.length());
+ if (lower >= wkt.length() || wkt.charAt(lower) !=
Symbols.FRAGMENT_VALUE) {
+ return new Element(this, wkt, position); // This is the usual
case.
+ }
/*
- * Aliases for fragments (e.g. "$Foo" in ProjectedCRS["something",
$Foo]) are expanded by
- * the `Element` constructor, except if the alias appears at the
begining of the text.
- * In such case the alias is the whole text and we need a different
constructor.
+ * Aliases for fragments (e.g. "FOO" in ProjectedCRS["something",
$FOO]) are expanded by `Element`
+ * constructor invoked above, except if the alias appears at the
begining of the WKT string.
+ * In such case the alias is the whole text and is handled in a
special way below.
*/
- Element fragment;
- int lower = CharSequences.skipLeadingWhitespaces(text,
position.getIndex(), text.length());
- if (lower < text.length() && text.charAt(lower) ==
Symbols.FRAGMENT_VALUE) {
- final int upper = endOfFragmentName(text, ++lower);
- final String id = text.substring(lower, upper);
- fragment = fragments.get(id); // Should be
immutable.
- if (fragment == null) {
- position.setErrorIndex(lower);
- throw new UnparsableObjectException(errorLocale,
Errors.Keys.NoSuchValue_1, new Object[] {id}, lower);
- }
- position.setIndex(upper);
- if (sharedValues == null) { // `true` if
invoked for immediate parsing.
- fragment = fragment.modifiable(); // Parsing
requires a modifiable copy.
- }
- } else {
- fragment = new Element(this, text, position, sharedValues);
+ final int upper = endOfFragmentName(wkt, ++lower);
+ final String id = wkt.substring(lower, upper);
+ StoredTree fragment = fragments.get(id);
+ if (fragment == null) {
+ position.setErrorIndex(--lower);
+ throw new UnparsableObjectException(errorLocale,
Errors.Keys.NoSuchValue_1, new Object[] {id}, lower);
}
- return fragment;
- }
-
- /**
- * Parses a tree of {@link Element}s to produce a geodetic object.
- * The {@code root} argument should be a value returned by
- * {@link #textToTree(String, ParsePosition, Map)}.
- *
- * @param root the tree of WKT elements.
- * @return the parsed object.
- * @throws ParseException if the tree can not be parsed.
- */
- final Object buildFromTree(Element root) throws ParseException {
- warnings = null;
- ignoredElements.clear();
- root = new Element("<root>", root);
- final Object object = parseObject(root);
- root.close(ignoredElements);
- return object;
- }
-
- /**
- * Parses a <cite>Well Know Text</cite> (WKT) as a geodetic object.
- *
- * @param text the text to be parsed.
- * @param position the position to start parsing from.
- * @return the parsed object.
- * @throws ParseException if the string can not be parsed.
- */
- public Object parseObject(final String text, final ParsePosition position)
throws ParseException {
- warnings = null;
- ignoredElements.clear();
- ArgumentChecks.ensureNonEmpty("text", text);
- Element root = textToTree(text, position, null);
- root = new Element("<root>", root);
- final Object object = parseObject(root);
- root.close(ignoredElements);
- return object;
+ position.setIndex(upper);
+ return fragment.toElement(this, ~0);
}
/**
@@ -340,7 +328,7 @@ abstract class AbstractParser implements Parser {
* @return the parsed object.
* @throws ParseException if the element can not be parsed.
*/
- abstract Object parseObject(final Element element) throws ParseException;
+ abstract Object buildFromTree(Element element) throws ParseException;
/**
* Parses the number at the given position.
@@ -378,7 +366,8 @@ abstract class AbstractParser implements Parser {
}
/**
- * Parses the given unit name or symbol.
+ * Parses the given unit name or symbol. Contrarily to other {@code
parseFoo()} methods,
+ * this method has no {@link ParsePosition} and expects the given string
to be the full unit symbol.
*/
final Unit<?> parseUnit(final String text) throws ParserException {
if (unitFormat == null) {
@@ -424,12 +413,13 @@ abstract class AbstractParser implements Parser {
* Returns the warnings, or {@code null} if none.
* This method clears the warnings after the call.
*
- * <p>The returned object is valid only before a new parsing starts. If a
longer lifetime is desired,
- * then the caller <strong>must</strong> invokes {@link
Warnings#publish()}.</p>
+ * <p>The returned object is valid only until a new parsing starts. If a
longer lifetime
+ * is desired, then the caller <strong>must</strong> invokes {@link
Warnings#publish()}.</p>
*
- * @param object the object that resulted from the parsing operation, or
{@code null}.
+ * @param result the object that resulted from the parsing operation, or
{@code null}.
+ * @return the warnings, or {@code null} if none.
*/
- final Warnings getAndClearWarnings(final Object object) {
+ final Warnings getAndClearWarnings(final Object result) {
Warnings w = warnings;
warnings = null;
if (w == null) {
@@ -437,8 +427,9 @@ abstract class AbstractParser implements Parser {
return null;
}
w = new Warnings(errorLocale, true, ignoredElements);
+ // Do not clear `ignoredElements` now.
}
- w.setRoot(object);
+ w.setRoot(result);
return w;
}
}
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java
index 938dcce..711170d 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java
@@ -20,11 +20,9 @@ import java.util.Date;
import java.util.Map;
import java.util.List;
import java.util.LinkedList;
-import java.util.ListIterator;
import java.util.Iterator;
import java.util.Collections;
import java.util.Locale;
-import java.io.Serializable;
import java.text.ParsePosition;
import java.text.ParseException;
import org.opengis.referencing.cs.CoordinateSystem;
@@ -35,7 +33,6 @@ import org.apache.sis.util.CharSequences;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.util.CollectionsExt;
-import org.apache.sis.internal.util.UnmodifiableArrayList;
import static org.apache.sis.util.CharSequences.skipLeadingWhitespaces;
@@ -50,7 +47,14 @@ import static
org.apache.sis.util.CharSequences.skipLeadingWhitespaces;
*
* Each {@code Element} object can contain an arbitrary amount of other
elements.
* The result is a tree, which can be seen with {@link #toString()} for
debugging purpose.
- * Elements can be pulled in a <cite>first in, first out</cite> order.
+ * Elements can be pulled by their name and other children (numbers, dates,
strings)
+ * can be pulled in a <cite>first in, first out</cite> order.
+ *
+ * <h2>Sharing repetitive information</h2>
+ * {@link Element} instances are mutable because {@link AbstractParser} needs
to remove elements from
+ * the {@link #children} list as they are processed. If that parsing does not
happen immediately,
+ * then {@code Element} content needs to be copied in a different structure
({@link StoredTree})
+ * which is immutable and designed for reducing redundancies.
*
* @author Rémi Ève (IRD)
* @author Martin Desruisseaux (IRD, Geomatys)
@@ -58,12 +62,7 @@ import static
org.apache.sis.util.CharSequences.skipLeadingWhitespaces;
* @since 0.6
* @module
*/
-final class Element implements Serializable {
- /**
- * Indirectly for {@link WKTFormat} serialization compatibility.
- */
- private static final long serialVersionUID = -7345192763818308443L;
-
+final class Element {
/**
* Kind of value expected in the element. Value 0 means "not yet
determined".
*/
@@ -79,123 +78,126 @@ final class Element implements Serializable {
};
/**
- * The position where this element starts in the string to be parsed.
+ * Index of the character where this element starts in the WKT string to
parse.
+ *
+ * @see #offsetAfterKeyword()
*/
final int offset;
/**
* Index of the keyword in the array given to the {@link #pullElement(int,
String...)} method.
+ * This is a workaround for the lack of multiple return values in Java.
*
* @see #getKeywordIndex()
*/
private byte keywordIndex;
/**
- * Keyword of this entity. For example: {@code "PrimeMeridian"}.
+ * Keyword of the WKT element, for example {@code "PrimeMeridian"}. Never
{@code null}.
*/
public final String keyword;
/**
* {@code true} if the keyword was not followed by a pair of brackets
(e.g. "north").
- * If {@code true}, then {@link #children} shall be an empty list and
{@link #isImmutable} should be {@code true}.
+ * If {@code true}, then {@link #children} shall be an empty list.
*/
private final boolean isEnumeration;
/**
- * Whether this element is immutable.
+ * {@code true} if this element has been reconstituted from {@link
WKTFormat#fragments}.
+ * In such case, all {@link #offset} values are identical.
*/
- private final boolean isImmutable;
+ final boolean isFragment;
/**
* An ordered sequence of {@link String}s, {@link Number}s and other
{@link Element}s.
* Access to this collection should be done using the iterator, not by
random access.
+ * Parsing will remove elements (in any order) from this list as they are
consumed.
+ *
+ * @see #getChildren()
*/
private final List<Object> children;
/**
* The locale to be used for formatting an error message if the parsing
fails, or {@code null} for
* the system default. This is <strong>not</strong> the locale for parting
number or date values.
+ *
+ * <div class="note"><b>Design note:</b>
+ * the same reference is duplicated in every {@code Element} instances. We
nevertheless copy it
+ * as a convenience for avoiding to make this argument appears in the
{@code pullFoo(…)} methods.</div>
*/
private final Locale errorLocale;
/**
- * Constructs a root element.
+ * Constructs a root element as a modifiable wrapper around the given
element.
+ * This wrapper is a convenience for branching on different codes
depending on
+ * the keyword value. For example:
+ *
+ * {@preformat java
+ * Element wrapper = new Element(an_element_with_unknown_keyword);
+ * Element e = wrapper.pullElement(…, "ProjectedCRS");
+ * if (e != null) {
+ * // Do something specific to projected CRS.
+ * return;
+ * }
+ * e = wrapper.pullElement(…, "GeographicCRS");
+ * if (e != null) {
+ * // Do something specific to Geographic CRS.
+ * return;
+ * }
+ * // etc.
+ * }
+ *
+ * @param singleton the only child for this root.
*
- * @param name an arbitrary name for the root element.
- * @param singleton the only child for this root.
+ * @see #pullElement(int, String...)
*/
- Element(final String name, final Element singleton) {
- keyword = name;
+ Element(Element singleton) {
+ keyword = "<root>"; // Ignored (any arbitrary name
is okay)
offset = singleton.offset;
errorLocale = singleton.errorLocale;
isEnumeration = false;
- isImmutable = false;
- children = new LinkedList<>(); // Needs to be
a modifiable collection.
- children.add(singleton.modifiable());
+ isFragment = false;
+ children = new LinkedList<>(); // Needs to be a modifiable
collection.
+ children.add(singleton);
}
/**
- * Creates a modifiable copy of the given element.
- * Modifiable instances are needed by the WKT parser.
+ * Creates a new node for the given keyword and list of children. This is
used by {@link StoredTree}
+ * for recreating a tree of {@link Element}s from a previously saved
snapshot.
*
- * @see #modifiable()
- */
- private Element(final Element toCopy) {
- offset = toCopy.offset;
- keyword = toCopy.keyword;
- errorLocale = toCopy.errorLocale;
- isEnumeration = toCopy.isEnumeration; // Should
always be `false`.
- isImmutable = isEnumeration;
- children = new LinkedList<>(toCopy.children); // Needs to be
a modifiable collection.
- final ListIterator<Object> it = children.listIterator();
- while (it.hasNext()) {
- final Object value = it.next();
- if (value instanceof Element) {
- final Element fragment = (Element) value;
- if (fragment.isImmutable) {
- it.set(new Element(fragment));
- }
- }
- }
- }
-
- /**
- * Returns a mutable instance of this {@code Element}.
- * If this element is already modifiable, then it is returned as-is.
- * If this element is unmodifiable, then a modifiable copy is created.
- */
- final Element modifiable() {
- return isImmutable ? new Element(this) : this;
+ * @param keyword keyword of the WKT element, e.g. {@code
"PrimeMeridian"}. Shall not be {@code null}.
+ * @param children children of this element, or {@code null} if this
element is an enumeration.
+ * @param offset index of the character where this element started in
the WKT string.
+ * If negative, actual offset is {@code ~offset} and {@link
#isFragment} is set to {@code true}.
+ */
+ // Children intentionally forced to LinkedList type for consistency with
next constructor.
+ Element(final String keyword, final LinkedList<Object> children, final int
offset, final Locale errorLocale) {
+ this.keyword = keyword;
+ this.isEnumeration = (children == null);
+ this.children = isEnumeration ? Collections.emptyList() :
children;
+ this.isFragment = (offset < 0);
+ this.offset = isFragment ? ~offset : offset;
+ this.errorLocale = errorLocale;
}
/**
- * Constructs a new {@code Element}.
- * The {@code sharedValues} argument have two meanings:
+ * Constructs a new {@code Element} by parsing the given WKT string
starting at the given position.
*
- * <ul class="verbose">
- * <li>If {@code null}, then the caller is parsing a WKT string. The
{@code Element}
- * must be mutable because its content will be emptied as the parsing
progress.</li>
- *
- * <li>If non-null, then the caller is storing a WKT fragment. We create
the elements but the caller will
- * not parse them immediately. The {@code Element} should be immutable
because the fragment will potentially
- * be reused many time. Since the fragment may be stored for a long
time, the {@code sharedValues} map will
- * be used for sharing unique instance of each value if possible.</li>
- * </ul>
- *
- * @param text the text to parse.
- * @param position on input, the position where to start parsing from.
- * On output, the first character after the separator.
- * @param sharedValues non-null if parsing a WKT tree to be kept for a
long time.
- * In such case, contains values found during parsing
of other elements.
- */
- Element(final AbstractParser parser, final String text, final
ParsePosition position,
- final Map<Object,Object> sharedValues) throws ParseException
- {
+ * @param parser information about symbols (such as brackets) and
formats to use.
+ * @param text the Well-Known Text (WKT) to parse.
+ * @param position on input, the position where to start parsing from.
+ * On output, the first character after the separator.
+ * @throws ParseException if quotes, brackets or parenthesis are not
balanced, or a date/number
+ * can not be parsed, or a referenced WKT fragment (e.g. {@code
"$FOO"}) can not be found.
+ */
+ Element(final AbstractParser parser, final String text, final
ParsePosition position) throws ParseException {
+ isFragment = false;
+ errorLocale = parser.errorLocale;
/*
* Find the first keyword in the specified string. If a keyword is
found, then
* the position is set to the index of the first character after the
keyword.
*/
- errorLocale = parser.errorLocale;
offset = position.getIndex();
final int length = text.length();
int lower = skipLeadingWhitespaces(text, offset, length);
@@ -227,9 +229,8 @@ final class Element implements Serializable {
openingBracket = text.codePointAt(lower))) < 0)
{
position.setIndex(lower);
- this.children = Collections.emptyList();
+ children = Collections.emptyList();
isEnumeration = true;
- isImmutable = true;
return;
}
lower = skipLeadingWhitespaces(text, lower +
Character.charCount(openingBracket), length);
@@ -242,32 +243,35 @@ final class Element implements Serializable {
* - Otherwise, if the first character is a unicode identifier
start, then the element is parsed as a chid Element.
* - Otherwise, if the first character is a quote, then the value is
taken as a String.
* - Otherwise, the element is parsed as a number or as a date,
depending of 'isTemporal' boolean value.
+ *
+ * A `LinkedList` implementation is suitable: we will always use
iterators (never random access)
+ * and the parser will delete elements at any point during iteration.
*/
- final List<Object> children = new LinkedList<>();
+ children = new LinkedList<>();
+ isEnumeration = false;
final String separator = parser.symbols.trimmedSeparator();
while (lower < length) {
final int firstChar = text.codePointAt(lower);
if (firstChar == Symbols.FRAGMENT_VALUE) {
/*
+ * ══════════ ALIAS
════════════════════════════════════════════════════════════════════════════════
* WKTFormat allows to substitute strings like "$FOO" by a WKT
fragment. This is something similar
* to environment variables in Unix. If we find the "$"
character, get the identifier behind "$"
* and insert the corresponding WKT fragment here.
*/
final int upper = AbstractParser.endOfFragmentName(text,
++lower);
- final String id = text.substring(lower, upper);
- Element fragment = parser.fragments.get(id);
+ final String id = text.substring(lower--, upper);
+ StoredTree fragment = parser.fragments.get(id);
if (fragment == null) {
position.setIndex(offset);
position.setErrorIndex(lower);
throw new UnparsableObjectException(errorLocale,
Errors.Keys.NoSuchValue_1, new Object[] {id}, lower);
}
- if (sharedValues == null) { // `true`
if created for immediate parsing.
- fragment = fragment.modifiable(); // WKT
parser needs modifiable elements.
- }
- children.add(fragment);
+ children.add(fragment.toElement(parser, ~lower));
// Set offset to '$' in "$FOO".
lower = upper;
} else if (Character.isUnicodeIdentifierStart(firstChar)) {
/*
+ * ══════════ ELEMENT or BOOLEAN
═══════════════════════════════════════════════════════════════════
* If the character is the beginning of a Unicode identifier,
add as a child element
* except for the boolean "true" and "false" values which are
handled in a special way.
*/
@@ -277,14 +281,16 @@ final class Element implements Serializable {
children.add(Boolean.FALSE);
} else {
position.setIndex(lower);
- children.add(new Element(parser, text, position,
sharedValues));
+ children.add(new Element(parser, text, position));
lower = position.getIndex();
}
} else {
- Object value;
+ // ══════════ PRIMITIVES (STRING, NUMBER, DATE, etc.)
══════════════════════════════════════════════
+ final Object value;
final int closingQuote =
parser.symbols.matchingQuote(firstChar);
if (closingQuote >= 0) {
/*
+ * ══════════ STRING
═══════════════════════════════════════════════════════════════════════════
* Try to parse the next element as a quoted string. We
will take it as a string if the first non-blank
* character is a quote. Note that a double quote means
that the quote should be included as-is in the
* parsed text.
@@ -310,19 +316,20 @@ final class Element implements Serializable {
}
((StringBuilder)
content).appendCodePoint(closingQuote).append(text, lower, upper);
}
- lower = upper + n; // After the closing quote.
+ lower = upper + n; // After
the closing quote.
} while (lower < text.length() && text.codePointAt(lower)
== closingQuote);
/*
- * Leading and trailing spaces should be ignored according
ISO 19162 §B.4.
+ * Leading and trailing spaces should be ignored according
ISO 19162 annex.
* Note that the specification suggests also to replace
consecutive white
* spaces by a single space, but we don't do that yet.
*/
value = CharSequences.trimWhitespaces(content).toString();
} else {
/*
+ * ══════════ NUMBER or DATE
═══════════════════════════════════════════════════════════════════
* Try to parse the next element as a date or a number. We
attempt such parsing when
* the first non-blank character is not the beginning of
an unicode identifier.
- * Otherwise we assume that the next element is the
keyword of a child 'Element'.
+ * Otherwise we assume that the next element is the
keyword of a child `Element`.
*/
position.setIndex(lower);
if (valueType == 0) {
@@ -339,15 +346,6 @@ final class Element implements Serializable {
}
lower = position.getIndex();
}
- /*
- * Store the value, using shared instances if this `Element`
may be stored for a long time.
- */
- if (sharedValues != null) {
- final Object e = sharedValues.putIfAbsent(value, value);
- if (e != null) {
- value = e;
- }
- }
children.add(value);
}
/*
@@ -362,10 +360,7 @@ final class Element implements Serializable {
final int c = text.codePointAt(lower);
if (c == closingBracket) {
position.setIndex(lower + Character.charCount(c));
- isEnumeration = false;
- isImmutable = (sharedValues != null);
- this.children = isImmutable ?
UnmodifiableArrayList.wrap(children.toArray()) : children;
- return;
+ return; // End of parsing does not
need to be end of string.
}
position.setErrorIndex(lower);
throw unparsableString(text, position);
@@ -402,7 +397,7 @@ final class Element implements Serializable {
* <code>"Error in <{@link #keyword}>"</code> will be prepend to the
message.
* The error index will be the starting index of this {@code Element}.
*
- * @param cause the cause of the failure, or {@code null} if none.
+ * @param cause the cause of the failure, or {@code null} if none.
* @return the exception to be thrown.
*/
final ParseException parseFailed(final Exception cause) {
@@ -413,8 +408,8 @@ final class Element implements Serializable {
/**
* Returns a {@link ParseException} with a "Unparsable string" message.
* The error message is built from the specified string starting at the
specified position.
- * Properties {@link ParsePosition#getIndex()} and {@link
ParsePosition#getErrorIndex()}
- * must be accurate before this method is invoked.
+ * The {@link ParsePosition#getErrorIndex()} property must be accurate
before this method is invoked.
+ * The {@link ParsePosition#getIndex()} property will be set by this
method.
*
* @param text the unparsable string.
* @param position the position in the string.
@@ -425,7 +420,7 @@ final class Element implements Serializable {
final CharSequence[] arguments;
final int errorIndex = Math.max(offset, position.getErrorIndex());
final int length = text.length();
- if (errorIndex == length) {
+ if (errorIndex >= length) {
errorKey = Errors.Keys.UnexpectedEndOfString_1;
arguments = new String[] {keyword};
} else {
@@ -439,9 +434,9 @@ final class Element implements Serializable {
/**
* Returns an exception saying that a character is missing.
*
- * @param c the missing character.
- * @param errorIndex the error position.
- * @param position the position to update with the error index.
+ * @param c the missing character.
+ * @param errorIndex the error position.
+ * @param position the position to update with the error index.
*/
private ParseException missingCharacter(final int c, final int errorIndex,
final ParsePosition position) {
position.setIndex(offset);
@@ -457,12 +452,8 @@ final class Element implements Serializable {
* @param key the name of the missing sub-element.
*/
final ParseException missingComponent(final String key) {
- int error = offset;
- if (keyword != null) {
- error += keyword.length();
- }
return new UnparsableObjectException(errorLocale,
Errors.Keys.MissingComponentInElement_2,
- new String[] {keyword, key}, error);
+ new String[] {keyword, key}, offsetAfterKeyword());
}
/**
@@ -517,6 +508,14 @@ final class Element implements Serializable {
return new UnparsableObjectException(errorLocale, key, new String[]
{value}, offset);
}
+ /**
+ * Returns index of the character after the keyword in the WKT string to
parse.
+ */
+ private int offsetAfterKeyword() {
+ if (isFragment) return offset;
+ return offset + keyword.length();
+ }
+
@@ -527,34 +526,6 @@ final class Element implements Serializable {
//////////////////////////////////////////////////////////////////////////////////////
/**
- * Returns the last element of the given names without removing it.
- * This method searches only in children of this element.
- * It does not search recursively in children of children.
- *
- * @param keys the element names (e.g. {@code "ID"}).
- * @return the last {@link Element} of the given names found in the
children, or {@code null} if none.
- *
- * @see #pullElement(int, String...)
- */
- public Element peekLastElement(final String... keys) {
- final ListIterator<Object> iterator =
children.listIterator(children.size());
- while (iterator.hasPrevious()) {
- final Object object = iterator.previous();
- if (object instanceof Element) {
- final Element element = (Element) object;
- if (!element.isEnumeration) {
- for (int i=0; i<keys.length; i++) {
- if (element.keyword.equalsIgnoreCase(keys[i])) {
- return element;
- }
- }
- }
- }
- }
- return null;
- }
-
- /**
* Returns the next value (not a child element) without removing it.
*
* @return the next value, or {@code null} if none.
@@ -571,25 +542,6 @@ final class Element implements Serializable {
}
/**
- * Returns the next values (not child elements) without removing them.
- * The maximum number of values fetched is the length of the given array.
- * If there is less WKT elements, remaining array elements are unchanged.
- *
- * @param addTo non-empty array where to store the values.
- */
- public void peekValues(final Object[] addTo) {
- int count = 0;
- final Iterator<Object> iterator = children.iterator();
- while (iterator.hasNext()) {
- final Object object = iterator.next();
- if (!(object instanceof Element)) {
- addTo[count] = object;
- if (++count >= addTo.length) break;
- }
- }
- }
-
- /**
* Removes the next {@link Date} from the children and returns it.
*
* @param key the parameter name. Used for formatting an error message
if no date is found.
@@ -795,6 +747,15 @@ final class Element implements Serializable {
}
/**
+ * Returns a copy of children list, or {@code null} if this {@code
Element} is an enumeration.
+ * This method is used only for creating a snapshot of this {@code
Element} in {@link StoredTree}.
+ * The returned array may contain nested {@link Element} instances.
+ */
+ final Object[] getChildren() {
+ return isEnumeration ? null : children.toArray();
+ }
+
+ /**
* Returns {@code true} if this element does not contains any remaining
child.
*
* @return {@code true} if there is no child remaining.
@@ -807,7 +768,7 @@ final class Element implements Serializable {
* Returns the index of the keyword in the array given to the {@link
#pullElement(int, String...)} method.
*/
final int getKeywordIndex() {
- return keywordIndex;
+ return Byte.toUnsignedInt(keywordIndex);
}
/**
@@ -830,7 +791,7 @@ final class Element implements Serializable {
CollectionsExt.addToMultiValuesMap(ignoredElements, ((Element)
value).keyword, keyword);
} else {
throw new UnparsableObjectException(errorLocale,
Errors.Keys.UnexpectedValueInElement_2,
- new Object[] {keyword, value}, offset +
keyword.length());
+ new Object[] {keyword, value}, offsetAfterKeyword());
}
}
}
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
index 257bbcb..66ee90d 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
@@ -207,7 +207,7 @@ class GeodeticObjectParser extends MathTransformParser
implements Comparator<Coo
* @param errorLocale the locale for error messages (not for parsing),
or {@code null} for the system default.
* @param factories on input, the factories to use. On output, the
factories used. Can be null.
*/
- GeodeticObjectParser(final Symbols symbols, final Map<String,Element>
fragments,
+ GeodeticObjectParser(final Symbols symbols, final Map<String,StoredTree>
fragments,
final NumberFormat numberFormat, final DateFormat dateFormat,
final UnitFormat unitFormat,
final Convention convention, final Transliterator transliterator,
final Locale errorLocale,
final ReferencingFactoryContainer factories)
@@ -228,7 +228,9 @@ class GeodeticObjectParser extends MathTransformParser
implements Comparator<Coo
}
/**
- * Parses a <cite>Well Know Text</cite> (WKT).
+ * Parses a <cite>Well-Know Text</cite> from specified position as a
geodetic object.
+ * Caller should invoke {@link #getAndClearWarnings(Object)} in a {@code
finally} block
+ * after this method.
*
* @param text the text to be parsed.
* @param position the position to start parsing from.
@@ -236,10 +238,10 @@ class GeodeticObjectParser extends MathTransformParser
implements Comparator<Coo
* @throws ParseException if the string can not be parsed.
*/
@Override
- public final Object parseObject(final String text, final ParsePosition
position) throws ParseException {
+ final Object createFromWKT(final String text, final ParsePosition
position) throws ParseException {
final Object object;
try {
- object = super.parseObject(text, position);
+ object = super.createFromWKT(text, position);
/*
* After parsing the object, we may have been unable to set the
VerticalCRS of VerticalExtent instances.
* First, try to set a default VerticalCRS for Mean Sea Level
Height in metres. In the majority of cases
@@ -281,7 +283,7 @@ class GeodeticObjectParser extends MathTransformParser
implements Comparator<Coo
* @throws ParseException if the element can not be parsed.
*/
@Override
- final Object parseObject(final Element element) throws ParseException {
+ final Object buildFromTree(final Element element) throws ParseException {
Object value = parseCoordinateReferenceSystem(element, false);
if (value != null) {
return value;
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
index fd0711f..c8a51f7 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
@@ -155,7 +155,7 @@ class MathTransformParser extends AbstractParser {
* @param factories the factories to use for creating math transforms
and geodetic objects.
* @param errorLocale the locale for error messages (not for parsing),
or {@code null} for the system default.
*/
- MathTransformParser(final Symbols symbols, final Map<String,Element>
fragments,
+ MathTransformParser(final Symbols symbols, final Map<String,StoredTree>
fragments,
final NumberFormat numberFormat, final DateFormat dateFormat,
final UnitFormat unitFormat,
final ReferencingFactoryContainer factories, final Locale
errorLocale)
{
@@ -181,7 +181,7 @@ class MathTransformParser extends AbstractParser {
* @throws ParseException if the element can not be parsed.
*/
@Override
- Object parseObject(final Element element) throws ParseException {
+ Object buildFromTree(final Element element) throws ParseException {
return parseMathTransform(element, true);
}
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/StoredTree.java
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/StoredTree.java
new file mode 100644
index 0000000..5800f5b
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/StoredTree.java
@@ -0,0 +1,483 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.io.wkt;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Stream;
+import java.io.Serializable;
+import org.apache.sis.util.ArraysExt;
+import org.apache.sis.internal.referencing.WKTKeywords;
+
+
+/**
+ * A tree of {@link Element}s saved for later use. {@code StoredTree}s are
created in following situations:
+ *
+ * <ul>
+ * <li>{@link WKTFormat#addFragment(String, String)} for defining shortcuts
+ * to be inserted into an arbitrary amount of other WKT strings.</li>
+ * <li>{@link WKTDictionary#addDefinitions(Stream)} for preparing WKT
definitions to be parsed
+ * only when first needed. While WKT trees are waiting, they may share
references to same
+ * {@code Node} instances for reducing memory usage.</li>
+ * </ul>
+ *
+ * This class does not store {@link Element} instances directly because {@code
Element}s are not easily shareable.
+ * Contrarily to {@code Element} design, {@code StoredTree} needs unmodifiable
{@link Element#children} list and
+ * needs to store {@link Element#offset} values in separated arrays. Those
changes make possible to have many
+ * {@code StoredTree} instances sharing the same {@code Node} instances in the
common case where some WKT elements
+ * are repeated in many trees.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+final class StoredTree implements Serializable {
+ /**
+ * Indirectly for {@link WKTFormat} serialization compatibility.
+ */
+ private static final long serialVersionUID = 8436779786449395346L;
+
+ /**
+ * Unmodifiable copy of {@link Element} without contextual information
such as {@link Element#offset}.
+ * The removal of contextual information increase greatly the possibility
to reuse the same {@code Node}
+ * instances in many {@link StoredTree}s. For example the {@code
UNIT["degrees", 0.0174532925199433]} node
+ * is repeated a lot, so we want to share only one {@code Node} instance
for every places in the WKT tree
+ * where degrees unit is declared, even if they appear at different
offsets in the WKT string.
+ *
+ * @see StoredTree#root
+ */
+ private static final class Node implements Serializable {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = 1463070931527783896L;
+
+ /**
+ * Copy of {@link Element#keyword} reference. Never {@code null}.
+ *
+ * @see StoredTree#keyword()
+ */
+ final String keyword;
+
+ /**
+ * Snapshot of {@link Element#children} list. Array content shall not
be modified.
+ * This array is {@code null} if the keyword was not followed by a
pair of brackets
+ * (e.g. "north"). A null value is not equivalent to an empty list.
For example the
+ * list is null when parsing {@code "FOO"} but is empty when parsing
{@code "FOO[]"}.
+ */
+ private final Object[] children;
+
+ /**
+ * Creates an immutable copy of the given element. Keywords and
children references
+ * are copied in this new {@code Node} but {@link Element#offset}s are
copied in a
+ * separated array for making possible to share {@code Node} instances.
+ */
+ Node(final Deflater deflater, final Element element) {
+ keyword = element.keyword;
+ children = element.getChildren();
+ if (children != null) {
+ for (int i=0; i<children.length; i++) {
+ final Object child = children[i];
+ if (child instanceof Element) {
+ children[i] = deflater.unique(new Node(deflater,
(Element) child));
+ }
+ }
+ }
+ deflater.addOffset(element);
+ }
+
+ /**
+ * Copies this node in a modifiable {@link Element}.
+ * This is the converse of the {@link #Node(Deflater, Element)}
constructor.
+ *
+ * @see StoredTree#toElement(AbstractParser, int)
+ */
+ final Element toElement(final Inflater inflater) {
+ final LinkedList<Object> list;
+ if (children == null) {
+ list = null;
+ } else {
+ list = new LinkedList<>();
+ for (Object child : children) {
+ if (child instanceof Node) {
+ child = ((Node) child).toElement(inflater);
+ }
+ list.add(child);
+ }
+ }
+ // Offsets must be read in the same order as they have been
written.
+ return new Element(keyword, list, inflater.nextOffset(),
inflater.errorLocale);
+ }
+
+ /**
+ * Returns the last element of the given names.
+ * This method searches only in children of this node.
+ * It does not search recursively in children of children.
+ *
+ * @param keys the element names (e.g. {@code "ID"}).
+ * @return the last {@link Node} of the given names found in the
children, or {@code null} if none.
+ */
+ final Node peekLastElement(final String... keys) {
+ if (children != null) {
+ for (int i = children.length; --i >= 0;) {
+ final Object value = children[i];
+ if (value instanceof Node) {
+ final Node node = (Node) value;
+ if (node.children != null) {
+ for (final String key : keys) {
+ if (node.keyword.equalsIgnoreCase(key)) {
+ return node;
+ }
+ }
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the next values (not child elements).
+ * The maximum number of values fetched is the length of the given
array.
+ * If there is less WKT elements, remaining array elements are
unchanged.
+ *
+ * @param addTo non-empty array where to store the values.
+ * @param index index where to store the first element in given
array.
+ */
+ final void peekValues(final Object[] addTo, int index) {
+ for (final Object object : children) {
+ if (!(object instanceof Node)) {
+ addTo[index] = object;
+ if (++index >= addTo.length) break;
+ }
+ }
+ }
+
+ /**
+ * Returns the string representation of the first value, which is
usually the element name.
+ * For example in {@code DATUM["WGS 84", …]} this is "WGS 84". If
there is no children then
+ * this method returns the keyword, which is usually an enumeration
value (for example "NORTH"}).
+ *
+ * @see StoredTree#toString()
+ */
+ @Override
+ public String toString() {
+ return (children != null && children.length != 0) ?
String.valueOf(children[0]) : keyword;
+ }
+
+ /**
+ * Returns a hash code value for this node. It uses hash codes of
child elements, except
+ * for children that are instances of {@link Node} for which identity
hash codes are used.
+ * We avoid requesting "normal" hash code of child {@link Node}
because the tree structure
+ * may cause the same hash codes to be computed many times. The use of
identity hash codes
+ * is sufficient if children have been replaced by unique instances
before to compute the
+ * hash code of this {@link Node}. This replacement is done by {@link
Deflater#unique(Node)}.
+ *
+ * @see Deflater#unique(Node)
+ */
+ @Override
+ public int hashCode() {
+ int hash = keyword.hashCode();
+ if (children != null) {
+ for (final Object value : children) {
+ hash = 31*hash + ((value instanceof Node) ?
System.identityHashCode(value) : value.hashCode());
+ }
+ }
+ return hash;
+ }
+
+ /**
+ * Returns whether the given object is equal to this {@code Node},
comparing keyword and children.
+ * Nested {@link Node}s are compared by identity comparisons; see
{@link #hashCode()} for rational.
+ *
+ * @see #hashCode()
+ * @see Deflater#unique(Node)
+ */
+ @Override
+ public boolean equals(final Object other) {
+ if (other instanceof Node) {
+ final Node that = (Node) other;
+ if (keyword.equals(that.keyword)) {
+ if (children == that.children) {
+ return true;
+ }
+ if (children != null && that.children != null &&
children.length == that.children.length) {
+ for (int i=0; i<children.length; i++) {
+ final Object value = children[i];
+ final Object otherValue = that.children[i];
+ if (!(value instanceof Node ? (value ==
otherValue) : value.equals(otherValue))) {
+ // Identity comparison of `Node` instances for
consistency with `hashCode()`.
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+ /**
+ * Root of a tree of {@link Element} snapshots.
+ */
+ private final Node root;
+
+ /**
+ * Indices in the WKT string where elements have been found. If negative,
the actual offset
+ * value is {@code ~offset} and {@link Element#isFragment} shall be set to
{@code true}.
+ * This array shall not be modified because it may be shared by many
{@link StoredTree}s.
+ *
+ * @see Deflater#addOffset(Element)
+ * @see Inflater#nextOffset()
+ */
+ private final short[] offsets;
+
+ /**
+ * Creates a new {@code StoredTree} with a copy of given arrays.
+ * Changes to the given array after construction will not affect this
{@code StoredTree}.
+ *
+ * @param root root of the tree of WKT elements.
+ * @param sharedValues pool to use for sharing unique instances of
values.
+ */
+ StoredTree(final Element root, final Map<Object,Object> sharedValues) {
+ final Deflater deflater = new Deflater(sharedValues);
+ this.root = deflater.unique(new Node(deflater, root));
+ offsets = deflater.offsets();
+ }
+
+ /**
+ * Recreates {@link Element} tree. This method is the converse of the
constructor.
+ *
+ * @param parser the parser which will be used for parsing the tree.
+ * @param isFragment non-zero if and only if {@link Element#isFragment}
shall be {@code true}.
+ * In such case, this value must be <code>~{@linkplain
Element#offset}</code>.
+ * @return root of {@link Element} tree.
+ */
+ final Element toElement(final AbstractParser parser, final int isFragment)
{
+ return root.toElement(new Inflater(parser, offsets, isFragment));
+ }
+
+ /**
+ * A helper class for compressing a tree of {@link Element}s as a tree of
{@link Node}s.
+ * Contrarily to {@code Element} instances, {@code Node}s instances can be
shared between many trees.
+ * Each instances shall be used for constructing only one {@link Node}.
After node construction, this
+ * instance lives longer in the {@link #sharedValues} map for sharing
{@link #offsets} arrays.
+ *
+ * @see StoredTree#StoredTree(Element, Map)
+ */
+ private static final class Deflater {
+ /**
+ * Pool to use for sharing unique instances of values.
+ * This is a copy of {@link WKTFormat#sharedValues} map.
+ * This is reset to {@code null} when not needed anymore.
+ */
+ private Map<Object,Object> sharedValues;
+
+ /**
+ * The {@link Element#offset} value of {@link StoredTree#root}
together with offsets of all
+ * {@link Element#children} in iteration order. Order is defined by
{@link Node} constructor.
+ * This array is expanded as needed. Shall not be modified after call
to {@link #offsets()}.
+ */
+ private short[] offsets;
+
+ /**
+ * Number of valid elements in {@link #offsets}.
+ */
+ private int count;
+
+ /**
+ * Pool of previously constructed values used for replacing equal
instances by unique instances.
+ * May contain {@link String}, {@link Long}, {@link Double} and {@link
Node} instances among others.
+ *
+ * @param sharedValues pool of previously created objects.
+ */
+ Deflater(final Map<Object,Object> sharedValues) {
+ this.sharedValues = sharedValues;
+ offsets = new short[24];
+ }
+
+ /**
+ * Returns a unique instance of given node.
+ *
+ * @return a previous instance from the pool, or {@code node} if none.
+ *
+ * @see Node#hashCode()
+ * @see Node#equals(Object)
+ */
+ final Node unique(final Node node) {
+ final Object existing = sharedValues.putIfAbsent(node, node);
+ return (existing != null) ? (Node) existing : node;
+ }
+
+ /**
+ * Adds the given {@link Element#offset} value.
+ *
+ * @see Inflater#nextOffset()
+ */
+ final void addOffset(final Element element) {
+ if (count >= offsets.length) {
+ offsets = Arrays.copyOf(offsets, count * 2);
+ }
+ int offset = Math.min(Short.MAX_VALUE, element.offset);
+ if (element.isFragment) offset = ~offset;
+ offsets[count++] = (short) offset;
+ }
+
+ /**
+ * Returns all {@link Element#offset} values in iteration order.
+ * This method may return an array shared by different {@link Node}
instances; do not modify.
+ */
+ @SuppressWarnings("ReturnOfCollectionOrArrayField")
+ final short[] offsets() {
+ offsets = ArraysExt.resize(offsets, count);
+ final Deflater other = (Deflater) sharedValues.putIfAbsent(this,
this);
+ sharedValues = null;
+ if (other != null) {
+ this.offsets = other.offsets;
+ }
+ return offsets;
+ }
+
+ /**
+ * Compares the {@link #offsets} arrays for equality. This is used by
{@link #offsets()}
+ * (indirectly, through {@link Map}) as a workaround for Java arrays
not overriding
+ * {@code equals(Object)} method.
+ */
+ @Override
+ public boolean equals(final Object other) {
+ return (other instanceof Deflater) && Arrays.equals(offsets,
((Deflater) other).offsets);
+ }
+
+ /**
+ * Computes a hash code value based only on the {@link #offsets} array.
+ */
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(offsets);
+ }
+ }
+
+ /**
+ * A helper class for decompressing a tree of {@link Element}s from a tree
of {@link Node}s.
+ * This is the converse of {@link Deflater}.
+ *
+ * @see StoredTree#toElement(AbstractParser, int)
+ */
+ private static final class Inflater {
+ /**
+ * If {@link Element#offset} must be fixed to a value, the bitwise NOT
value of that offset.
+ * Otherwise 0. This field packs two information:
+ *
+ * <ul>
+ * <li>{@link Element#isFragment} = ({@code isFragment} != 0)</li>
+ * <li>If {@code isFragment} is {@code true}, then:
+ * <ul><li>{@link Element#offset} = {@code ~isFragment}</li></ul>
+ * </li>
+ * </ul>
+ */
+ private final int isFragment;
+
+ /**
+ * The {@link StoredTree#offsets} array. Shall not be modified because
potentially shared.
+ * Ignored if {@link #isFragment} != 0.
+ */
+ private final short[] offsets;
+
+ /**
+ * Index of the next offset to return in the {@link #offsets} array.
+ * Ignored if {@link #isFragment} != 0.
+ */
+ private int index;
+
+ /**
+ * Locale to use for producing error message.
+ */
+ final Locale errorLocale;
+
+ /**
+ * Creates a new inflater.
+ *
+ * @param parser the parser which will be used for parsing the
tree.
+ * @param offsets the {@link StoredTree#offsets} array. Will not
be modified.
+ * @param isFragment non-zero if and only if {@link
Element#isFragment} is {@code true}.
+ * In such case, this value must be
<code>~{@linkplain Element#offset}</code>.
+ */
+ Inflater(final AbstractParser parser, final short[] offsets, final int
isFragment) {
+ this.errorLocale = parser.errorLocale;
+ this.isFragment = isFragment;
+ this.offsets = offsets;
+ }
+
+ /**
+ * Returns the value to assign to {@link Element#offset} for the next
element.
+ */
+ final int nextOffset() {
+ return (isFragment != 0) ? isFragment : offsets[index++];
+ }
+ }
+
+ /**
+ * Stores identifier information in the given array. This method locates
the last {@code "ID"} (WKT 2)
+ * or {@code "AUTHORITY"} (WKT 1) node and optionally the {@code
"CITATION"} sub-node. Values are copied
+ * in the given array, in that order!
+ *
+ * <ol>
+ * <li>Code space</li>
+ * <li>Code</li>
+ * <li>Version if present</li>
+ * <li>Authority if present (skipped if the array length is less than
4)</li>
+ * </ol>
+ *
+ * If any of above values is missing, the corresponding array element is
left unchanged.
+ * Callers should set all array elements to {@code null} before to invoke
this method.
+ *
+ * @param fullId where to store code space, code, version, authority.
+ */
+ final void peekIdentifiers(final Object[] fullId) {
+ Node id = root.peekLastElement(MathTransformParser.ID_KEYWORDS);
+ if (id != null) {
+ id.peekValues(fullId, 0);
+ if (fullId.length >= 4) {
+ id = id.peekLastElement(WKTKeywords.Citation);
+ if (id != null) {
+ id.peekValues(fullId, 3);
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns the keyword of the root element.
+ */
+ final String keyword() {
+ return root.keyword;
+ }
+
+ /**
+ * Returns the string representation of the first value of the root
element, which is usually the element name.
+ * For example in {@code DATUM["WGS 84", …]} this is "WGS 84". If there is
no children then this method returns
+ * the keyword, which is usually an enumeration value (for example
"NORTH"}).
+ */
+ @Override
+ public String toString() {
+ return root.toString();
+ }
+}
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
index 62a0840..a20ff85 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
@@ -146,14 +146,14 @@ public class WKTDictionary extends
GeodeticAuthorityFactory {
* Values can be one of the following 4 types:
*
* <ol>
- * <li>{@link Element}: this is the initial state when there is no
duplicated codes.
+ * <li>{@link StoredTree}: this is the initial state when there is no
duplicated codes.
* This is the root of a tree of WKT keywords with their values as
children.
* A tree can be parsed later as an {@link IdentifiedObject} when
first requested.</li>
- * <li>{@link IdentifiedObject}: the result of parsing the root {@link
Element}
+ * <li>{@link IdentifiedObject}: the result of parsing the {@link
StoredTree}
* when {@link #createObject(String)} is invoked for a given
authority code.
- * The parsing result replaces the previous {@link Element}
value.</li>
+ * The parsing result replaces the previous {@link StoredTree}
value.</li>
* <li>{@link Disambiguation}: if the same code is used by two or more
authorities or versions,
- * then above-cited {@link Element} or {@link IdentifiedObject}
alternatives are wrapped
+ * then above-cited {@link StoredTree} or {@link IdentifiedObject}
alternatives are wrapped
* in a {@link Disambiguation} object.</li>
* <li>{@link String} if parsing failed, in which case the string is the
error message.</li>
* </ol>
@@ -162,7 +162,7 @@ public class WKTDictionary extends GeodeticAuthorityFactory
{
* All read operations in this map shall be synchronized by the
<code>{@linkplain #lock}.readLock()</code>
* and write operations synchronized by the <code>{@linkplain
#lock}.writeLock()</code>.
*
- * @see #addDefinition(Element)
+ * @see #addDefinition(StoredTree)
* @see #createObject(String)
*/
private final Map<String,Object> definitions;
@@ -191,9 +191,9 @@ public class WKTDictionary extends GeodeticAuthorityFactory
{
private final String version;
/**
- * The value as an {@link Element} before parsing or an {@link
IdentifiedObject} after parsing.
- * They are the kind of types documented in {@link
WKTDictionary#definitions}, excluding
- * other {@code Disambiguation} instances.
+ * The value as an {@link StoredTree} before parsing or an {@link
IdentifiedObject} after parsing.
+ * They are the kind of types documented in {@link
WKTDictionary#definitions}, excluding other
+ * {@code Disambiguation} instances.
*/
Object value;
@@ -222,9 +222,9 @@ public class WKTDictionary extends GeodeticAuthorityFactory
{
* @param object definition in WKT of the CRS (or other geodetic
object) to wrap.
* @param fullId an array of length 3 to be used for getting the
{@code codespace:version:code} tuple.
*/
- Disambiguation(final Element object, final Object[] fullId) {
+ Disambiguation(final StoredTree object, final Object[] fullId) {
Arrays.fill(fullId, null);
-
object.peekLastElement(MathTransformParser.ID_KEYWORDS).peekValues(fullId);
+ object.peekIdentifiers(fullId);
codespace = trimOrNull(fullId[0]);
version = trimOrNull(fullId[2]);
value = object;
@@ -241,10 +241,10 @@ public class WKTDictionary extends
GeodeticAuthorityFactory {
* @param value the CRS (or other geodetic object) definition.
* @throws IllegalArgumentException if
<var>authority:version:code</var> identifier is already used.
*
- * @see WKTDictionary#addDefinition(Element)
+ * @see WKTDictionary#addDefinition(StoredTree)
*/
Disambiguation(Disambiguation previous, final String codespace, final
String version,
- final String code, final Element value)
+ final String code, final StoredTree value)
{
this.previous = previous;
this.codespace = codespace;
@@ -588,17 +588,17 @@ public class WKTDictionary extends
GeodeticAuthorityFactory {
if (buffer.length() != 0) {
pos.setIndex(0);
final String wkt = buffer.toString();
- final Element root = parser.textToTree(wkt, pos);
+ final StoredTree tree = parser.textToTree(wkt, pos);
final int end = pos.getIndex();
if (end < wkt.length()) { // Trailing white spaces
already removed by `read(…)`.
throw new
FactoryDataException(resources().getString(Resources.Keys.UnexpectedTextAtLine_2,
getLineNumber(), CharSequences.token(wkt,
end)));
}
if (aliasKey != null) {
- parser.addFragment(aliasKey, root);
+ parser.addFragment(aliasKey, tree);
aliasKey = null;
} else {
- addDefinition(root);
+ addDefinition(tree);
}
buffer.setLength(0);
}
@@ -611,48 +611,41 @@ public class WKTDictionary extends
GeodeticAuthorityFactory {
* Caller must own the write lock before to invoke this method.
* {@link #updateAuthority()} should be invoked after this method.
*
- * @param root root of a tree of WKT elements.
+ * @param tree a tree of WKT elements.
* @throws IllegalArgumentException if a {@code codespace:version:code}
tuple is assigned twice.
* @throws FactoryDataException if the WKT does not have an {@code ID[…]}
or {@code AUTHORITY[…]} element.
*
* @see #definitions
*/
- private void addDefinition(final Element root) throws FactoryDataException
{
- final Element ide =
root.peekLastElement(MathTransformParser.ID_KEYWORDS);
- if (ide != null) {
- final Object[] fullId = new Object[3]; //
Codespace, code, version.
- ide.peekValues(fullId);
- final String codespace = trimOrNull(fullId[0]);
- final String code = trimOrNull(fullId[1]);
- if (code != null) { //
Needs at least the code.
- definitions.merge(code, root, (oldValue, newValue) -> {
- final String version = trimOrNull(fullId[2]);
- final Disambiguation previous;
- if (oldValue instanceof Disambiguation) {
- previous = (Disambiguation) oldValue;
- } else if (oldValue instanceof Element) {
- previous = new Disambiguation((Element) oldValue,
fullId);
- } else if (oldValue instanceof IdentifiedObject) {
- previous = new Disambiguation((IdentifiedObject)
oldValue);
- } else {
- previous = null; // Discard previous parsing
failure.
- }
- return new Disambiguation(previous, codespace, version,
code, (Element) newValue);
- });
- codespaces.add(codespace);
- if (authorities != null) {
- final Element citation =
ide.peekLastElement(WKTKeywords.Citation);
- if (citation != null) {
- final String title = trimOrNull(citation.peekValue());
- if (title != null) {
- authorities.add(title);
- }
- }
- }
- return;
+ private void addDefinition(final StoredTree tree) throws
FactoryDataException {
+ final Object[] fullId = new Object[authorities == null ? 4 : 3];
+ tree.peekIdentifiers(fullId); // Codespace, code,
version, (authority).
+ final String code = trimOrNull(fullId[1]);
+ if (code == null) {
+ throw new
FactoryDataException(resources().getString(Resources.Keys.MissingAuthorityCode_1,
tree));
+ }
+ final String codespace = trimOrNull(fullId[0]);
+ definitions.merge(code, tree, (oldValue, newValue) -> {
+ final String version = trimOrNull(fullId[2]);
+ final Disambiguation previous;
+ if (oldValue instanceof Disambiguation) {
+ previous = (Disambiguation) oldValue;
+ } else if (oldValue instanceof StoredTree) {
+ previous = new Disambiguation((StoredTree) oldValue, fullId);
+ } else if (oldValue instanceof IdentifiedObject) {
+ previous = new Disambiguation((IdentifiedObject) oldValue);
+ } else {
+ previous = null; // Discard previous parsing failure.
+ }
+ return new Disambiguation(previous, codespace, version, code,
(StoredTree) newValue);
+ });
+ codespaces.add(codespace);
+ if (fullId.length >= 4) {
+ final String title = trimOrNull(fullId[3]);
+ if (title != null) {
+ authorities.add(title);
}
}
- throw new
FactoryDataException(resources().getString(Resources.Keys.MissingAuthorityCode_1,
root.peekValue()));
}
/**
@@ -697,6 +690,7 @@ public class WKTDictionary extends GeodeticAuthorityFactory
{
throw new FactoryDataException(resources().getString(
Resources.Keys.CanNotParseWKT_2, lineNumber,
e.getLocalizedMessage()));
} finally {
+ parser.clear();
updateAuthority();
}
} finally {
@@ -792,8 +786,8 @@ public class WKTDictionary extends GeodeticAuthorityFactory
{
}
/*
* At this point we separated codespace, code and version. First,
verify that codespace is valid.
- * Then get CRS definition as an `IdentifiedObject` or an `Element`
(the `Disambiguation` case is
- * resolved as an `IdentifiedObject` or `Element`).
+ * Then get CRS definition as an `IdentifiedObject` or an `StoredTree`
(the `Disambiguation` case
+ * is resolved as an `IdentifiedObject` or `StoredTree`).
*/
Disambiguation choices = null;
Object value = null;
@@ -823,15 +817,15 @@ public class WKTDictionary extends
GeodeticAuthorityFactory {
/*
* At this point we got a value which may be one of the following
classes:
*
- * - `Element` — if this method is invoked for the first
time for the given code.
+ * - `StoredTree` — if this method is invoked for the first
time for the given code.
* - `IdentifiedObject` — if we already built the geodetic object in
a previous invocation of this method.
* - `String` — if a previous invocation for given code
failed to build the geodetic object.
* In this case, the string is the exception
message.
*
- * If `Element`, try to replace that value by an `IdentifiedObject`
(on success) or `String` (on failure).
+ * If `StoredTree`, try to replace that value by an `IdentifiedObject`
(on success) or `String` (on failure).
* Must be done under write lock because `parser` is not thread-safe.
*/
- if (value instanceof Element) {
+ if (value instanceof StoredTree) {
lock.writeLock().lock();
try {
if (choices != null) {
@@ -839,10 +833,10 @@ public class WKTDictionary extends
GeodeticAuthorityFactory {
} else {
value = definitions.get(localCode);
}
- if (value instanceof Element) {
+ if (value instanceof StoredTree) {
ParseException cause = null;
try {
- value = parser.parse((Element) value);
+ value = parser.buildFromTree((StoredTree) value);
} catch (ParseException e) {
cause = e;
value = e.getLocalizedMessage();
@@ -898,8 +892,8 @@ public class WKTDictionary extends GeodeticAuthorityFactory
{
final String[] keywords = WKTKeywords.forType(type);
final Class<? extends IdentifiedObject> baseType = type;
// Because lambdas require final.
final Predicate<Object> filter = (element) -> {
- if (element instanceof Element) {
- return (keywords == null) ||
ArraysExt.containsIgnoreCase(keywords, ((Element) element).keyword);
+ if (element instanceof StoredTree) {
+ return (keywords == null) ||
ArraysExt.containsIgnoreCase(keywords, ((StoredTree) element).keyword());
} else {
return baseType.isInstance(element);
}
@@ -920,7 +914,7 @@ public class WKTDictionary extends GeodeticAuthorityFactory
{
}
/*
* Verify if an existing collection (assigned to another
type) provides the same values.
- * If we find one, we will share the same instances.
+ * If we find one, share the same instance for reducing
memory usage.
*/
boolean share = false;
for (final Set<String> other : codeCaches.values()) {
@@ -931,6 +925,8 @@ public class WKTDictionary extends GeodeticAuthorityFactory
{
}
}
if (!share) {
+ // TODO: replace by Set.copyOf(Set) in JDK9 and remove
the `share` flag
+ // (not needed because Set.copyOf(Set) does the
verification itself).
codes = CollectionsExt.unmodifiableOrCopy(codes);
}
codeCaches.put(type, codes);
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java
index 6b03397..95a78af 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java
@@ -202,11 +202,11 @@ public class WKTFormat extends CompoundFormat<Object> {
/**
* WKT fragments that can be inserted in longer WKT strings, or {@code
null} if none. Keys are short identifiers
* and values are WKT subtrees to substitute to the identifiers when they
are found in a WKT to parse.
- * The same map instance may be shared by different {@linkplain #clone()
clones}.
+ * The same map instance may be shared by different {@linkplain #clone()
clones} as long as they are not modified.
*
* @see #fragments(boolean)
*/
- private Map<String,Element> fragments;
+ private Map<String,StoredTree> fragments;
/**
* {@code true} if the {@link #fragments} map is shared by two or more
{@code WKTFormat} instances.
@@ -284,7 +284,7 @@ public class WKTFormat extends CompoundFormat<Object> {
* @param modifiable whether the caller intents to modify the map.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
- private Map<String,Element> fragments(final boolean modifiable) {
+ private Map<String,StoredTree> fragments(final boolean modifiable) {
if (fragments == null) {
if (!modifiable) {
// Most common cases: invoked before to parse a WKT and no
fragments specified.
@@ -761,42 +761,52 @@ public class WKTFormat extends CompoundFormat<Object> {
throw new
IllegalArgumentException(errors().getString(Errors.Keys.NotAUnicodeIdentifier_1,
name));
}
final ParsePosition pos = new ParsePosition(0);
- final Element element = textToTree(wkt, pos);
+ final StoredTree definition = textToTree(wkt, pos);
final int length = wkt.length();
final int index = CharSequences.skipLeadingWhitespaces(wkt,
pos.getIndex(), length);
if (index < length) {
throw new UnparsableObjectException(getErrorLocale(),
Errors.Keys.UnexpectedCharactersAfter_2,
- new Object[] {name + " = " + element.keyword + "[…]",
CharSequences.token(wkt, index)}, index);
+ new Object[] {name + " = " + definition.keyword() + "[…]",
CharSequences.token(wkt, index)}, index);
}
- addFragment(name, element);
+ addFragment(name, definition);
}
/**
* Adds a fragment of Well Know Text (WKT).
* Caller must have verified that {@code name} is a valid Unicode
identifier.
*
- * @param name the Unicode identifier to assign to the WKT fragment.
- * @param element root of the WKT fragment to add.
+ * @param name the Unicode identifier to assign to the WKT
fragment.
+ * @param definition root of the WKT fragment to add.
* @throws IllegalArgumentException if a fragment is already associated to
the given name.
*/
- final void addFragment(final String name, final Element element) {
- if (fragments(true).putIfAbsent(name, element) != null) {
+ final void addFragment(final String name, final StoredTree definition) {
+ if (fragments(true).putIfAbsent(name, definition) != null) {
throw new
IllegalArgumentException(errors().getString(Errors.Keys.ElementAlreadyPresent_1,
name));
}
}
/**
- * Parses a fragment of Well Know Text (WKT).
+ * Parses a Well Know Text (WKT) for a fragment or an entire object
definition.
+ * This method should be invoked only for WKT trees to be stored for a
long time.
+ * It should not be invoked for immediate {@link IdentifiedObject} parsing.
*
* @param wkt the Well Know Text (WKT) fragment to parse.
* @param pos index of the first character to parse (on input) or after
last parsed character (on output).
* @return root of the tree of elements.
*/
- final Element textToTree(final String wkt, final ParsePosition pos) throws
ParseException {
+ final StoredTree textToTree(final String wkt, final ParsePosition pos)
throws ParseException {
+ final AbstractParser parser = parser(true);
+ Element result = null;
+ warnings = null;
+ try {
+ result = parser.textToTree(wkt, pos);
+ } finally {
+ warnings = parser.getAndClearWarnings(result);
+ }
if (sharedValues == null) {
sharedValues = new HashMap<>();
}
- return parser(true).textToTree(wkt, pos, sharedValues);
+ return new StoredTree(result, sharedValues);
}
/**
@@ -824,32 +834,35 @@ public class WKTFormat extends CompoundFormat<Object> {
ArgumentChecks.ensureNonEmpty("wkt", wkt);
ArgumentChecks.ensureNonNull ("pos", pos);
final AbstractParser parser = parser(false);
- Object object = null;
+ Object result = null;
try {
- object = parser.parseObject(wkt.toString(), pos);
+ result = parser.createFromWKT(wkt.toString(), pos);
} finally {
- warnings = parser.getAndClearWarnings(object);
+ warnings = parser.getAndClearWarnings(result);
}
- return object;
+ return result;
}
/**
- * Creates an object from the given tree of WKT elements.
+ * Parses a tree of {@link Element}s to produce a geodetic object. The
{@code root} argument
+ * should be a value returned by {@link #textToTree(String,
ParsePosition)}.
*
- * @param root the tree of WKT elements.
+ * @param tree the tree of WKT elements.
* @return the parsed object (never {@code null}).
- * @throws ParseException if an error occurred while parsing the WKT.
+ * @throws ParseException if the tree can not be parsed.
*/
- final Object parse(final Element root) throws ParseException {
+ final Object buildFromTree(StoredTree tree) throws ParseException {
clear();
final AbstractParser parser = parser(false);
- Object object = null;
+ final Element root = new Element(tree.toElement(parser, 0));
+ Object result = null;
try {
- object = parser.buildFromTree(root);
+ result = parser.buildFromTree(root);
+ root.close(parser.ignoredElements);
} finally {
- warnings = parser.getAndClearWarnings(object);
+ warnings = parser.getAndClearWarnings(result);
}
- return object;
+ return result;
}
/**
@@ -881,7 +894,7 @@ public class WKTFormat extends CompoundFormat<Object> {
* for the source of logging messages which is the enclosing {@code
WKTParser} instead than a factory.
*/
private static final class Parser extends GeodeticObjectParser {
- Parser(final Symbols symbols, final Map<String,Element> fragments,
+ Parser(final Symbols symbols, final Map<String,StoredTree> fragments,
final NumberFormat numberFormat, final DateFormat dateFormat,
final UnitFormat unitFormat,
final Convention convention, final Transliterator
transliterator, final Locale errorLocale,
final ReferencingFactoryContainer factories)
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/doc-files/ESRI.txt
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/doc-files/ESRI.txt
index c1b56f3..dacde57 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/doc-files/ESRI.txt
+++
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/doc-files/ESRI.txt
@@ -35,7 +35,7 @@
# Alias for WGS84 geographic CRS.
# Can be inserted in projected CRS with "$WGS84".
#
-SET WGS84 =
+SET WGS84_BASE =
BaseGeodCRS["GCS_WGS_1984",
Datum["D_WGS_1984",
Ellipsoid["WGS_1984", 6378137, 298.257223563]],
@@ -47,7 +47,7 @@ SET WGS84 =
# when they have the default value.
#
ProjectedCRS["North_Pole_Stereographic",
- $WGS84,
+ $WGS84_BASE,
Conversion["Stereographic North Pole",
Method["Polar Stereographic (variant A)"],
Parameter["Latitude of natural origin", 90]],
@@ -58,7 +58,7 @@ ProjectedCRS["North_Pole_Stereographic",
Id["ESRI", 102018]]
ProjectedCRS["South_Pole_Stereographic",
- $WGS84,
+ $WGS84_BASE,
Conversion["Stereographic South Pole",
Method["Polar Stereographic (variant A)"],
Parameter["Latitude of natural origin", -90]],
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
index 33250aa..3245f66 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
@@ -1641,6 +1641,7 @@ public class GeodeticObjectFactory extends
AbstractFactory implements CRSFactory
*/
@Override
public CoordinateReferenceSystem createFromWKT(final String text) throws
FactoryException {
+ ArgumentChecks.ensureNonEmpty("text", text);
Parser p = parser.getAndSet(null);
if (p == null) try {
Constructor<? extends Parser> c = parserConstructor;
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
index 4190e33..f98830c 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/transform/DefaultMathTransformFactory.java
@@ -1537,6 +1537,7 @@ public class DefaultMathTransformFactory extends
AbstractFactory implements Math
@Override
public MathTransform createFromWKT(final String text) throws
FactoryException {
lastMethod.remove();
+ ArgumentChecks.ensureNonEmpty("text", text);
Parser p = parser.getAndSet(null);
if (p == null) try {
Constructor<? extends Parser> c = parserConstructor;
diff --git
a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ElementTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ElementTest.java
index e8f9395..254deeb 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ElementTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ElementTest.java
@@ -34,7 +34,7 @@ import static org.junit.Assert.*;
* Tests the {@link Element} class.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.6
+ * @version 1.1
* @since 0.6
* @module
*/
@@ -49,25 +49,19 @@ public final strictfp class ElementTest extends TestCase {
throw new UnsupportedOperationException();
}
- @Override Object parseObject(Element element) throws ParseException {
+ @Override Object buildFromTree(Element element) throws ParseException {
throw new UnsupportedOperationException();
}
};
/**
- * The map of shared values to gives to the {@link Element} constructor.
- * This is usually null, except for the test of WKT fragments.
- */
- private Map<Object,Object> sharedValues;
-
- /**
* Parses the given text and ensures that {@link ParsePosition} index is
set at to the end of string.
*/
private Element parse(final String text) throws ParseException {
final ParsePosition position = new ParsePosition(0);
final Element element;
try {
- element = new Element(parser, text, position, sharedValues);
+ element = new Element(parser, text, position);
} catch (ParseException e) {
assertEquals("index should be unchanged.", 0, position.getIndex());
assertTrue("Error index should be set.", position.getErrorIndex()
> 0);
@@ -296,27 +290,14 @@ public final strictfp class ElementTest extends TestCase {
@Test
@DependsOnMethod({"testPullString", "testPullElement"})
public void testFragments() throws ParseException {
- sharedValues = new HashMap<>();
- Element frag = parse("Frag[“A”,“B”,“A”]");
- parser.fragments.put("MyFrag", frag);
- try {
- frag.pullString("A");
- fail("Element shall be unmodifiable.");
- } catch (UnsupportedOperationException e) {
- // This is the expected exception.
- }
- /*
- * Parse a normal value. Since this is not a fragment,
- * we should be able to pull a copy of the components.
- */
- sharedValues = null;
+ parser.fragments.put("MyFrag", new
StoredTree(parse("Frag[“A”,“B”,“A”]"), new HashMap<>()));
final Element element = parse("Foo[“C”,$MyFrag,“D”]");
assertEquals("C", element.pullString("C"));
assertEquals("D", element.pullString("D"));
- frag = element.pullElement(AbstractParser.MANDATORY, "Frag");
+ Element frag = element.pullElement(AbstractParser.MANDATORY, "Frag");
final String a = frag.pullString("A");
assertEquals("A", a);
assertEquals("B", frag.pullString("B"));
- assertSame(a, frag.pullString("A")); // 'sharedValues' should have
allowed to share the same instance.
+ assertEquals(a, frag.pullString("A"));
}
}
diff --git
a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
index c5e3b04..e3368e0 100644
---
a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
+++
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
@@ -56,7 +56,7 @@ import static
org.apache.sis.internal.util.StandardDateFormat.MILLISECONDS_PER_D
* Tests {@link GeodeticObjectParser}.
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
* @since 0.6
* @module
*/
@@ -101,7 +101,7 @@ public final strictfp class GeodeticObjectParserTest
extends TestCase {
newParser(Convention.DEFAULT);
}
final ParsePosition position = new ParsePosition(0);
- final Object obj = parser.parseObject(text, position);
+ final Object obj = parser.createFromWKT(text, position);
assertEquals("errorIndex", -1, position.getErrorIndex());
assertEquals("index", text.length(), position.getIndex());
assertInstanceOf("GeodeticObjectParser.parseObject", type, obj);
diff --git
a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/MathTransformParserTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/MathTransformParserTest.java
index 7728b78..baef439 100644
---
a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/MathTransformParserTest.java
+++
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/MathTransformParserTest.java
@@ -38,7 +38,7 @@ import static org.opengis.test.Assert.*;
* Tests {@link MathTransformParser}.
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
* @since 0.6
* @module
*/
@@ -78,7 +78,7 @@ public final strictfp class MathTransformParserTest extends
TestCase {
assertEquals(DefaultMathTransformFactory.class.getCanonicalName(),
parser.getPublicFacade());
}
final ParsePosition position = new ParsePosition(0);
- final MathTransform mt = (MathTransform) parser.parseObject(text,
position);
+ final MathTransform mt = (MathTransform) parser.createFromWKT(text,
position);
assertEquals("errorIndex", -1, position.getErrorIndex());
assertEquals("index", text.length(), position.getIndex());
return mt;
diff --git
a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java
index 5ae1678..56001f0 100644
---
a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java
+++
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java
@@ -28,7 +28,6 @@ import org.opengis.referencing.crs.GeographicCRS;
import org.opengis.referencing.crs.GeodeticCRS;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.IdentifiedObject;
-import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.test.DependsOn;
import org.apache.sis.test.TestCase;
import org.junit.Test;
@@ -61,28 +60,34 @@ public final strictfp class WKTDictionaryTest extends
TestCase {
factory.load(source);
}
/*
- * ESRI code space should be fist because it is the most frequently
used
- * in the test file. The authority should be "ESRI" for the same
reason.
- * Codes can be in any order.
+ * TEST code space should be fist because it is the most frequently
used
+ * in the test file. The authority should be "TEST" for the same
reason.
+ * Codes can be in any order. Code spaces are omitted when there is no
ambiguity.
*/
- assertArrayEquals("getCodeSpaces()", new String[] {"ESRI",
"MyCodeSpace"}, factory.getCodeSpaces().toArray());
- assertSame("getAuthority()", Citations.ESRI, factory.getAuthority());
+ assertArrayEquals("getCodeSpaces()", new String[] {"TEST", "ESRI"},
factory.getCodeSpaces().toArray());
+ assertEquals("getAuthority()", "TEST",
factory.getAuthority().getTitle().toString());
Set<String> codes = factory.getAuthorityCodes(IdentifiedObject.class);
- assertSame(codes, factory.getAuthorityCodes(IdentifiedObject.class));
// Test caching.
- assertSame(codes, factory.getAuthorityCodes(SingleCRS.class));
// Test sharing.
- assertSetEquals(Arrays.asList("102018", "ESRI::102021",
"MyCodeSpace::102021", "MyCodeSpace:v2:102021"), codes);
+ assertSame( codes,
factory.getAuthorityCodes(IdentifiedObject.class)); // Test caching.
+ assertSame( codes, factory.getAuthorityCodes(SingleCRS.class));
// Test sharing.
+ assertSetEquals(Arrays.asList("102018", "ESRI::102021",
"TEST::102021", "TEST:v2:102021", "E1", "E2"), codes);
assertSetEquals(Arrays.asList("102018", "ESRI::102021"),
factory.getAuthorityCodes(ProjectedCRS.class));
codes = factory.getAuthorityCodes(GeographicCRS.class);
- assertSetEquals(Arrays.asList("MyCodeSpace::102021",
"MyCodeSpace:v2:102021"), codes);
+ assertSetEquals(Arrays.asList("TEST::102021", "TEST:v2:102021", "E1",
"E2"), codes);
assertSame(codes, factory.getAuthorityCodes(GeodeticCRS.class));
// Test sharing.
assertSame(codes, factory.getAuthorityCodes(GeographicCRS.class));
// Test caching.
/*
* Tests CRS creation.
*/
- verifyCRS(factory.createProjectedCRS( "102018"),
"North_Pole_Stereographic", +90);
- verifyCRS(factory.createProjectedCRS("ESRI:102021"),
"South_Pole_Stereographic", -90);
- verifyCRS(factory.createGeographicCRS("MyCodeSpace::102021"),
"Anguilla 1957");
- verifyCRS(factory.createGeographicCRS("MyCodeSpace:v2:102021"),
"Anguilla 1957 (bis)");
+ verifyCRS(factory.createProjectedCRS ( "102018"),
"North_Pole_Stereographic", +90);
+ verifyCRS(factory.createProjectedCRS ("ESRI : 102021"),
"South_Pole_Stereographic", -90);
+ verifyCRS(factory.createGeographicCRS("TEST: :102021"), "Anguilla
1957");
+ verifyCRS(factory.createGeographicCRS("TEST:v2:102021"), "Anguilla
1957 (bis)");
+ /*
+ * Test creation of CRS having errors.
+ * - Verify error index.
+ */
+ verifyErroneousCRS(factory, "E1", 69);
+ verifyErroneousCRS(factory, "E2", 42);
}
/**
@@ -111,4 +116,49 @@ public final strictfp class WKTDictionaryTest extends
TestCase {
assertAxisDirectionsEqual(name, crs.getCoordinateSystem(),
AxisDirection.NORTH, AxisDirection.EAST);
}
+
+ /**
+ * Verifies the error message and error offset when trying to parse an
erroneous CRS.
+ *
+ * @param factory factory to use.
+ * @param code code of erroneous CRS.
+ * @param errorOffset expected error index.
+ */
+ private static void verifyErroneousCRS(final WKTDictionary factory, final
String code, final int errorOffset) {
+ String details = null;
+ try {
+ factory.createGeographicCRS(code);
+ fail("Parsing should have failed.");
+ } catch (FactoryException e) {
+ /*
+ * Expect a message like: Can not create a geodetic object for
"E1".
+ * The exact message is locale-dependent, so we can not test fully.
+ */
+ final String message = e.getMessage();
+ assertTrue(message, message.contains(code));
+ /*
+ * Expect a message like: Missing "semiMajorAxis" component in
"Ellipsoid" element.
+ * The error offset (zero-based) should point to the character
after "Ellipsoid" in
+ * the following WKT:
+ *
+ * Datum["Erroneous", Ellipsoid["Missing axis length"]]
+ */
+ final UnparsableObjectException cause =
(UnparsableObjectException) e.getCause();
+ details = cause.getMessage();
+ assertTrue(message, details.contains("Ellipsoid"));
+ assertTrue(message, details.contains("semiMajorAxis"));
+ assertEquals("errorOffset", errorOffset, cause.getErrorOffset());
+ }
+ /*
+ * Try parsing again. The exception message should have been saved,
+ * i.e. the parsing process is not repeated.
+ */
+ try {
+ factory.createGeographicCRS(code);
+ fail("Parsing should have failed.");
+ } catch (FactoryException e) {
+ assertEquals(details, e.getMessage());
+ assertNull(e.getCause());
+ }
+ }
}
diff --git
a/core/sis-referencing/src/test/resources/org/apache/sis/io/wkt/ExtraCRS.txt
b/core/sis-referencing/src/test/resources/org/apache/sis/io/wkt/ExtraCRS.txt
index 06e7eb9..816038e 100644
--- a/core/sis-referencing/src/test/resources/org/apache/sis/io/wkt/ExtraCRS.txt
+++ b/core/sis-referencing/src/test/resources/org/apache/sis/io/wkt/ExtraCRS.txt
@@ -7,7 +7,7 @@
# Alias for WGS84 geographic CRS.
#
SET DEGREE = Unit["Degree", 0.0174532925199433]
-SET WGS84 =
+SET WGS84_BASE =
BaseGeodCRS["GCS_WGS_1984",
Datum["D_WGS_1984",
Ellipsoid["WGS_1984", 6378137, 298.257223563]],
@@ -20,7 +20,7 @@ SET WGS84 =
# when they have the default value.
#
ProjectedCRS["North_Pole_Stereographic",
- $WGS84,
+ $WGS84_BASE,
Conversion["Stereographic North Pole",
Method["Polar Stereographic (variant A)"],
Parameter["Latitude of natural origin", 90]],
@@ -31,7 +31,7 @@ ProjectedCRS["North_Pole_Stereographic",
Id["ESRI", 102018]]
ProjectedCRS["South_Pole_Stereographic",
- $WGS84,
+ $WGS84_BASE,
Conversion["Stereographic South Pole",
Method["Polar Stereographic (variant A)"],
Parameter["Latitude of natural origin", -90]],
@@ -41,25 +41,49 @@ ProjectedCRS["South_Pole_Stereographic",
Unit["metre", 1],
Id["ESRI", 102021]]
+
#
-# A dummy CRS using the same code than ESRI::102021 but
+# Dummy CRS using the same code than ESRI::102021 but
# different code space and versions. Used for testing
# resolution of code collisions.
#
GeodCRS["Anguilla 1957",
- Datum["Anguilla 1957",
- Ellipsoid["Clarke 1880", 6378249.145, 293.465]],
+ Datum["Anguilla 1957",
+ Ellipsoid["Clarke 1880", 6378249.145, 293.465]],
CS[ellipsoidal, 2],
Axis["Latitude", north],
Axis["Longitude", east],
$DEGREE,
- Id["MyCodeSpace", 102021]]
+ Id["TEST", 102021]]
GeodCRS["Anguilla 1957 (bis)",
- Datum["Anguilla 1957",
- Ellipsoid["Clarke 1880", 6378249.145, 293.465]],
+ Datum["Anguilla 1957",
+ Ellipsoid["Clarke 1880", 6378249.145, 293.465]],
+ CS[ellipsoidal, 2],
+ Axis["Latitude", north],
+ Axis["Longitude", east],
+ $DEGREE,
+ Id["TEST", 102021, "v2"]]
+
+
+#
+# Intentionally malformed CRS for testing error indices reported in
`ParseException`.
+# The erroneous element should be on the first line for avoiding
platform-dependency
+# caused by various line separators ("\n" versus "\r\n").
+#
+
+SET BAD_DATUM = Datum["Erroneous", Ellipsoid["Missing axis length"]]
+
+GeodCRS["Error index 69 (on Ellipsoid)", Datum["Erroneous", Ellipsoid["Missing
axis length"]],
+ CS[ellipsoidal, 2],
+ Axis["Latitude", north],
+ Axis["Longitude", east],
+ $DEGREE,
+ Id["TEST", "E1"]]
+
+GeodCRS["Error index 42 (on $BAD_DATUM)", $BAD_DATUM,
CS[ellipsoidal, 2],
Axis["Latitude", north],
Axis["Longitude", east],
$DEGREE,
- Id["MyCodeSpace", 102021, "v2"]]
+ Id["TEST", "E2"]]