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

commit 5e13b7f7bad086a32e228cf9f7ce66d52d9adec9
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Nov 10 00:11:53 2020 +0100

    Initial version of `WKTDictionary` for allowing users to define CRS for 
custom codes.
    https://issues.apache.org/jira/browse/SIS-502
---
 .../apache/sis/internal/referencing/Resources.java |  22 +-
 .../sis/internal/referencing/Resources.properties  |   6 +-
 .../internal/referencing/Resources_fr.properties   |   6 +-
 .../java/org/apache/sis/io/wkt/AbstractParser.java |  75 +-
 .../main/java/org/apache/sis/io/wkt/Element.java   | 105 ++-
 .../java/org/apache/sis/io/wkt/WKTDictionary.java  | 884 +++++++++++++++++++++
 .../main/java/org/apache/sis/io/wkt/WKTFormat.java |  90 ++-
 .../java/org/apache/sis/io/wkt/doc-files/ESRI.txt  |  69 ++
 .../java/org/apache/sis/io/wkt/package-info.java   |   4 +-
 .../org/apache/sis/io/wkt/WKTDictionaryTest.java   | 104 +++
 .../sis/test/suite/ReferencingTestSuite.java       |   1 +
 .../resources/org/apache/sis/io/wkt/ExtraCRS.txt   |  65 ++
 .../java/org/apache/sis/internal/jdk9/JDK9.java    |  14 +-
 .../java/org/apache/sis/internal/util/Strings.java |  25 +
 14 files changed, 1405 insertions(+), 65 deletions(-)

diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
index a71a1ea..5571276 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.java
@@ -127,6 +127,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short CanNotParseCombinedReference_2 = 78;
 
         /**
+         * Can not read Well-Known Text at line {0}. Caused by: {1}
+         */
+        public static final short CanNotParseWKT_2 = 96;
+
+        /**
          * Can not separate the “{0}” coordinate reference system into 
sub-components.
          */
         public static final short CanNotSeparateCRS_1 = 84;
@@ -158,7 +163,7 @@ public final class Resources extends IndexedResourceBundle {
         public static final short CanNotTransformGeometry = 86;
 
         /**
-         * Can not use the {0} geodetic parameters: {1}
+         * Can not use the {0} geodetic parameters. Caused by: {1}
          */
         public static final short CanNotUseGeodeticParameters_2 = 9;
 
@@ -346,6 +351,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short MisnamedParameter_1 = 38;
 
         /**
+         * Missing or empty “ID[…]” element for “{0}”.
+         */
+        public static final short MissingAuthorityCode_1 = 99;
+
+        /**
          * No authority was specified for code “{0}”. The expected syntax is 
“AUTHORITY:CODE”.
          */
         public static final short MissingAuthority_1 = 39;
@@ -507,6 +517,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short StartOrEndPointNotSet_1 = 88;
 
         /**
+         * Syntax error for Well-Known Text alias at line {0}.
+         */
+        public static final short SyntaxErrorForAlias_1 = 97;
+
+        /**
          * Combined URI contains unexpected components.
          */
         public static final short UnexpectedComponentInURI = 80;
@@ -517,6 +532,11 @@ public final class Resources extends IndexedResourceBundle 
{
         public static final short UnexpectedDimensionForCS_1 = 64;
 
         /**
+         * Unexpected text “{1}” at line {0}. WKT for new object should start 
with a non-indented line.
+         */
+        public static final short UnexpectedTextAtLine_2 = 98;
+
+        /**
          * Parameter “{0}” does not expect unit.
          */
         public static final short UnitlessParameter_1 = 65;
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
index 338d505..7432bcc 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources.properties
@@ -59,10 +59,11 @@ CanNotParseCombinedReference_2    = Can not parse component 
{1} in the combined
 CanNotSeparateCRS_1               = Can not separate the \u201c{0}\u201d 
coordinate reference system into sub-components.
 CanNotSeparateTransform_3         = Can not separate the transform because 
result would have {2} {0,choice,0#source|1#target} dimension{2,choice,1#|2#s} 
instead of {1}.
 CanNotSeparateTargetDimension_1   = Target dimension {0} depends on excluded 
source dimensions.
+CanNotParseWKT_2                  = Can not read Well-Known Text at line {0}. 
Caused by: {1}
 CanNotTransformCoordinates_2      = Can not transform the ({0,number}, 
{1,number}) coordinates.
 CanNotTransformEnvelopeToGeodetic = Can not transform envelope to a geodetic 
reference system.
 CanNotTransformGeometry           = Can not transform the given geometry.
-CanNotUseGeodeticParameters_2     = Can not use the {0} geodetic parameters: 
{1}
+CanNotUseGeodeticParameters_2     = Can not use the {0} geodetic parameters. 
Caused by: {1}
 ColinearAxisDirections_2          = Axis directions {0} and {1} are colinear.
 CoordinateOperationNotFound_2     = Coordinate conversion of transformation 
from system \u201c{0}\u201d to \u201c{1}\u201d has not been found.
 DatumChangesDirectory_1           = Datum shift files are searched in the 
\u201c{0}\u201d directory.
@@ -86,6 +87,7 @@ MismatchedParameterDescriptor_1   = Mismatched descriptor for 
\u201c{0}\u201d pa
 MismatchedPrimeMeridian_2         = Expected the \u201c{0}\u201d prime 
meridian but found \u201c{1}\u201d.
 MismatchedTransformDimension_3    = The transform has {2} 
{0,choice,0#source|1#target} dimension{2,choice,1#|2#s}, while {1} was expected.
 MissingAuthority_1                = No authority was specified for code 
\u201c{0}\u201d. The expected syntax is \u201cAUTHORITY:CODE\u201d.
+MissingAuthorityCode_1            = Missing or empty \u201cID[\u2026]\u201d 
element for \u201c{0}\u201d.
 MissingInterpolationOrdinates     = Not enough dimension in 
\u2018MathTransform\u2019 input or output coordinates for the interpolation 
points.
 MissingHorizontalDimension_1      = No horizontal dimension found in 
\u201c{0}\u201d.
 MissingVerticalDimension_1        = No vertical dimension found in 
\u201c{0}\u201d
@@ -113,8 +115,10 @@ ParameterNotFound_2               = No parameter named 
\u201c{1}\u201d has been
 RecursiveCreateCallForCode_2      = Recursive call while creating an object of 
type \u2018{0}\u2019 for code \u201c{1}\u201d.
 SingularMatrix                    = Matrix is singular.
 StartOrEndPointNotSet_1           = The {0,choice,0#start|1#end} point has not 
been specified.
+SyntaxErrorForAlias_1             = Syntax error for Well-Known Text alias at 
line {0}.
 UnexpectedComponentInURI          = Combined URI contains unexpected 
components.
 UnexpectedDimensionForCS_1        = Unexpected dimension for a coordinate 
system of type \u2018{0}\u2019.
+UnexpectedTextAtLine_2            = Unexpected text \u201c{1}\u201d at line 
{0}. WKT for new object should start with a non-indented line.
 UnitlessParameter_1               = Parameter \u201c{0}\u201d does not expect 
unit.
 UnknownAuthority_1                = Authority \u201c{0}\u201d is unknown.
 UnknownAxisDirection_1            = Axis direction \u201c{0}\u201d is unknown.
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
index 67abae0..3672c41 100644
--- 
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Resources_fr.properties
@@ -64,10 +64,11 @@ CanNotSeparateCRS_1               = Ne peut pas 
s\u00e9parer le syst\u00e8me de
 CanNotSeparateTransform_3         = Ne peut pas s\u00e9parer la transformation 
parce-que le r\u00e9sultat aurait {2} dimension{2,choice,1#|2#s} en 
{0,choice,0#entr\u00e9|1#sortie} au lieu de {1}.
 CanNotSeparateTargetDimension_1   = La dimension de destination {0} 
d\u00e9pend de dimensions sources qui ont \u00e9t\u00e9 exclues.
 CanNotParseCombinedReference_2    = Ne peut pas d\u00e9coder la composante {1} 
dans l\u2019{0,choice,0#URN|1#URL} combin\u00e9.
+CanNotParseWKT_2                  = Ne peut pas lire le \u00ab\u202fWell-Known 
Text\u202f\u00bb \u00e0 la ligne {0}. La cause est\u202f: {1}
 CanNotTransformCoordinates_2      = Ne peut pas transformer les 
coordonn\u00e9es ({0,number}; {1,number}).
 CanNotTransformEnvelopeToGeodetic = Ne peut pas transformer l\u2019enveloppe 
vers un r\u00e9f\u00e9rentiel g\u00e9od\u00e9sique.
 CanNotTransformGeometry           = Ne peut pas transformer la 
g\u00e9om\u00e9trie donn\u00e9e.
-CanNotUseGeodeticParameters_2     = Ne peut pas utiliser les param\u00e8tres 
g\u00e9od\u00e9siques {0}\u202f: {1}
+CanNotUseGeodeticParameters_2     = Ne peut pas utiliser les param\u00e8tres 
g\u00e9od\u00e9siques {0}. La cause est\u202f: {1}
 ColinearAxisDirections_2          = Les directions d\u2019axes {0} et {1} sont 
colin\u00e9aires.
 CoordinateOperationNotFound_2     = La conversion ou transformation des 
coordonn\u00e9es du syst\u00e8me \u00ab\u202f{0}\u202f\u00bb vers 
\u00ab\u202f{1}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e.
 DatumChangesDirectory_1           = Les fichiers de changements de 
r\u00e9f\u00e9rentiel sont cherch\u00e9s dans le dossier 
\u00ab\u202f{0}\u202f\u00bb.
@@ -91,6 +92,7 @@ MismatchedParameterDescriptor_1   = Le descripteur du 
param\u00e8tre \u00ab\u202
 MismatchedPrimeMeridian_2         = Le m\u00e9ridien d\u2019origine 
\u00ab\u202f{0}\u202f\u00bb \u00e9tait attendu, mais 
\u00ab\u202f{1}\u202f\u00bb a \u00e9t\u00e9 trouv\u00e9.
 MismatchedTransformDimension_3    = La {0,choice,0#source|1#destination} de la 
transformation a {2} dimension{2,choice,1#|2#s}, alors qu\u2019on en attendait 
{1}.
 MissingAuthority_1                = Aucune autorit\u00e9 n\u2019a 
\u00e9t\u00e9 sp\u00e9cifi\u00e9e pour le code \u00ab\u202f{0}\u202f\u00bb. Le 
format attendu est \u00ab\u202fAUTORIT\u00c9:CODE\u202f\u00bb.
+MissingAuthorityCode_1            = L\u2019\u00e9l\u00e9ment 
\u00ab\u202fID[\u2026]\u202f\u00bb est manquant ou vide pour 
\u00ab\u202f{0}\u202f\u00bb.
 MissingInterpolationOrdinates     = La dimension des coordonn\u00e9es en 
entr\u00e9 ou en sortie du \u2018MathTransform\u2019 n\u2019est pas suffisante 
pour contenir les points d\u2019interpolation.
 MissingHorizontalDimension_1      = Aucune dimension horizontale n\u2019a 
\u00e9t\u00e9 trouv\u00e9e dans \u00ab\u202f{0}\u202f\u00bb.
 MissingVerticalDimension_1        = Aucune dimension verticale n\u2019a 
\u00e9t\u00e9 trouv\u00e9e dans \u00ab\u202f{0}\u202f\u00bb.
@@ -118,8 +120,10 @@ ParameterNotFound_2               = Aucun param\u00e8tre 
nomm\u00e9 \u00ab\u202f
 RecursiveCreateCallForCode_2      = Appels r\u00e9cursifs lors de la 
cr\u00e9ation d\u2019un objet de type \u2018{0}\u2019 pour le code 
\u00ab\u202f{1}\u202f\u00bb.
 SingularMatrix                    = La matrice est singuli\u00e8re.
 StartOrEndPointNotSet_1           = Le point {0,choice,0#de 
d\u00e9part|1#d\u2019arriv\u00e9} n\u2019a pas \u00e9t\u00e9 d\u00e9fini.
+SyntaxErrorForAlias_1             = Erreur de syntaxe pour l\u2019alias de 
\u00ab\u202fWell-Known Text\u202f\u00bb \u00e0 la ligne {0}.
 UnexpectedComponentInURI          = L\u2019URI combin\u00e9 contient des 
composantes qui n\u2019\u00e9taient pas attendues.
 UnexpectedDimensionForCS_1        = Dimension inattendue pour un syst\u00e8me 
de coordonn\u00e9es de type \u2018{0}\u2019.
+UnexpectedTextAtLine_2            = Le texte \u00ab\u202f{1}\u202f\u00bb 
\u00e0 la ligne {0} est inattendu. Le WKT d\u2019un nouvel objet doit commencer 
par une ligne non-indent\u00e9e.
 UnitlessParameter_1               = Le param\u00e8tre 
\u00ab\u202f{0}\u202f\u00bb n\u2019attend pas d\u2019unit\u00e9.
 UnknownAuthority_1                = L\u2019autorit\u00e9 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas reconnue.
 UnknownAxisDirection_1            = La direction d\u2019axe 
\u00ab\u202f{0}\u202f\u00bb n\u2019est pas reconnue.
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 85596cd..b255587 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
@@ -254,40 +254,87 @@ abstract class AbstractParser implements Parser {
     }
 
     /**
-     * Parses a <cite>Well Know Text</cite> (WKT).
+     * Parses a <cite>Well Know Text</cite> (WKT) 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.
-     * @return the parsed 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.
+     * @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)
      */
-    public Object parseObject(final String text, final ParsePosition position) 
throws ParseException {
-        warnings = null;
-        ignoredElements.clear();
-        ArgumentChecks.ensureNonEmpty("text", text);
+    final Element textToTree(final String text, final ParsePosition position, 
final Map<Object,Object> sharedValues)
+            throws ParseException
+    {
+        /*
+         * 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.
+         */
         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);
+            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);
-            fragment = new Element(fragment);
+            if (sharedValues == null) {                         // `true` if 
invoked for immediate parsing.
+                fragment = fragment.modifiable();               // Parsing 
requires a modifiable copy.
+            }
         } else {
-            fragment = new Element(this, text, position, null);
+            fragment = new Element(this, text, position, sharedValues);
         }
-        final Element element = new Element("<root>", fragment);
-        final Object object = parseObject(element);
-        element.close(ignoredElements);
+        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;
     }
 
     /**
      * Parses the next element in the specified <cite>Well Know Text</cite> 
(WKT) tree.
+     * Subclasses will typically get the name of the first element and 
delegate to a specialized method
+     * such as {@code parseAxis(…)}, {@code parseEllipsoid(…)}, {@code 
parseTimeDatum(…)}, <i>etc</i>.
      *
      * @param  element  the element to be parsed.
      * @return the parsed object.
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 137203e..938dcce 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
@@ -62,7 +62,7 @@ final class Element implements Serializable {
     /**
      * Indirectly for {@link WKTFormat} serialization compatibility.
      */
-    private static final long serialVersionUID = 4048095121452884024L;
+    private static final long serialVersionUID = -7345192763818308443L;
 
     /**
      * Kind of value expected in the element. Value 0 means "not yet 
determined".
@@ -97,11 +97,16 @@ final class Element implements Serializable {
 
     /**
      * {@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.
+     * If {@code true}, then {@link #children} shall be an empty list and 
{@link #isImmutable} should be {@code true}.
      */
     private final boolean isEnumeration;
 
     /**
+     * Whether this element is immutable.
+     */
+    private final boolean isImmutable;
+
+    /**
      * 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.
      */
@@ -120,29 +125,34 @@ final class Element implements Serializable {
      * @param singleton  the only child for this root.
      */
     Element(final String name, final Element singleton) {
-        keyword     = name;
-        offset      = singleton.offset;
-        errorLocale = singleton.errorLocale;
-        children    = new LinkedList<>();                       // Needs to be 
a modifiable collection.
-        children.add(singleton);
+        keyword       = name;
+        offset        = singleton.offset;
+        errorLocale   = singleton.errorLocale;
         isEnumeration = false;
+        isImmutable   = false;
+        children      = new LinkedList<>();                     // Needs to be 
a modifiable collection.
+        children.add(singleton.modifiable());
     }
 
     /**
      * Creates a modifiable copy of the given element.
+     * Modifiable instances are needed by the WKT parser.
+     *
+     * @see #modifiable()
      */
-    Element(final Element toCopy) {
+    private Element(final Element toCopy) {
         offset        = toCopy.offset;
         keyword       = toCopy.keyword;
-        isEnumeration = toCopy.isEnumeration;
         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.isEnumeration) {
+                if (fragment.isImmutable) {
                     it.set(new Element(fragment));
                 }
             }
@@ -150,6 +160,15 @@ final class Element implements Serializable {
     }
 
     /**
+     * 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;
+    }
+
+    /**
      * Constructs a new {@code Element}.
      * The {@code sharedValues} argument have two meanings:
      *
@@ -166,7 +185,8 @@ final class Element implements Serializable {
      * @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  if parsing a fragment, a map with the values found 
in other elements. Otherwise {@code null}.
+     * @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
@@ -207,8 +227,9 @@ final class Element implements Serializable {
                                 openingBracket = text.codePointAt(lower))) < 0)
         {
             position.setIndex(lower);
-            children = Collections.emptyList();
+            this.children = Collections.emptyList();
             isEnumeration = true;
+            isImmutable   = true;
             return;
         }
         lower = skipLeadingWhitespaces(text, lower + 
Character.charCount(openingBracket), length);
@@ -240,8 +261,8 @@ final class Element implements Serializable {
                     position.setErrorIndex(lower);
                     throw new UnparsableObjectException(errorLocale, 
Errors.Keys.NoSuchValue_1, new Object[] {id}, lower);
                 }
-                if (!fragment.isEnumeration) {
-                    fragment = new Element(fragment);
+                if (sharedValues == null) {                         // `true` 
if created for immediate parsing.
+                    fragment = fragment.modifiable();               // WKT 
parser needs modifiable elements.
                 }
                 children.add(fragment);
                 lower = upper;
@@ -319,7 +340,7 @@ final class Element implements Serializable {
                     lower = position.getIndex();
                 }
                 /*
-                 * Store the value, using shared instances if this Element may 
be stored for a long time.
+                 * 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);
@@ -341,12 +362,9 @@ final class Element implements Serializable {
                 final int c = text.codePointAt(lower);
                 if (c == closingBracket) {
                     position.setIndex(lower + Character.charCount(c));
-                    if (sharedValues != null) {
-                        this.children = 
UnmodifiableArrayList.wrap(children.toArray());
-                    } else {
-                        this.children = children;
-                    }
                     isEnumeration = false;
+                    isImmutable   = (sharedValues != null);
+                    this.children = isImmutable ? 
UnmodifiableArrayList.wrap(children.toArray()) : children;
                     return;
                 }
                 position.setErrorIndex(lower);
@@ -509,6 +527,34 @@ 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.
@@ -525,6 +571,25 @@ 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.
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
new file mode 100644
index 0000000..0f69a1f
--- /dev/null
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
@@ -0,0 +1,884 @@
+/*
+ * 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.Locale;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.stream.Stream;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.io.LineNumberReader;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import org.opengis.util.FactoryException;
+import org.opengis.metadata.Identifier;
+import org.opengis.metadata.citation.Citation;
+import org.opengis.referencing.IdentifiedObject;
+import org.opengis.referencing.NoSuchAuthorityCodeException;
+import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
+import org.apache.sis.referencing.factory.FactoryDataException;
+import org.apache.sis.internal.referencing.Resources;
+import org.apache.sis.internal.referencing.WKTKeywords;
+import org.apache.sis.internal.util.CollectionsExt;
+import org.apache.sis.internal.util.Constants;
+import org.apache.sis.internal.util.Strings;
+import org.apache.sis.internal.jdk9.JDK9;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Exceptions;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.collection.Containers;
+import org.apache.sis.util.collection.FrequencySortedSet;
+
+
+/**
+ * A factory providing CRS objects parsed from WKT definitions associated to 
authority codes.
+ * Each WKT definition is associated to a key according the 
<var>authority:version:code</var>
+ * pattern where <var>code</var> is mandatory and <var>authority:version</var> 
are optional.
+ * Coordinate Reference Systems or other kinds of objects are created from WKT 
definitions
+ * when a {@code create(…)} method is invoked for the first time for a given 
key.
+ *
+ * <p>Newly constructed {@code WKTDictionary} are initially empty. For 
populating the factory,
+ * the {@link #load(BufferedReader)} or {@link #addDefinitions(Stream)} 
methods must be invoked.</p>
+ *
+ * <h2>Errors management</h2>
+ * Well-Known Text parsing is performed in two steps, each of them executed at 
a different time:
+ *
+ * <h3>Early validation</h3>
+ * WKT strings added by {@code load(…)} or {@code addDefinitions(…)} methods 
are verified
+ * for matching quotes, balanced parenthesis or brackets, and valid number or 
date formats.
+ * If a syntax error is detected, the loading process is interrupted at the 
point the error occurred;
+ * CRS definitions after the error location are not loaded.
+ * However WKT keywords and geodetic parameters (e.g. map projections) are not 
validated at this stage.
+ *
+ * <h3>Late validation</h3>
+ * WKT keywords and geodetic parameters inside WKT elements are validated only 
when {@link #createObject(String)}
+ * is invoked. If an error occurs at this stage, only the CRS (or other 
geodetic object) for the code given to
+ * the {@code createFoo(…)} method become invalid. Objects associated to other 
codes are not impacted.
+ *
+ * <h2>Multi-threading</h2>
+ * This class is thread-safe but not necessarily concurrent.
+ * This class is designed for a relatively small amount of WKT;
+ * it is not a replacement for database-backed factory such as
+ * {@link org.apache.sis.referencing.factory.sql.EPSGFactory}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public class WKTDictionary extends GeodeticAuthorityFactory {
+    /**
+     * The organization or specification that defines the codes recognized by 
this factory.
+     * May be {@code null} if not yet determined.
+     *
+     * @see #updateAuthority()
+     * @see #getAuthority()
+     */
+    private Citation authority;
+
+    /**
+     * Authorities declared in all {@code "ID[CITATION[…]]"} elements found in 
WKT definitions.
+     * This set is {@code null} if an {@link #authority} value has been 
explicitly specified at
+     * construction time. If non-null, this is used for creating a default 
{@link #authority}.
+     */
+    private final Set<String> authorities;
+
+    /**
+     * Code spaces of authority codes recognized by this factory.
+     * This set is computed from the {@code "ID[…]"} elements found in WKT 
definitions.
+     *
+     * @see #getCodeSpaces()
+     */
+    private final Set<String> codespaces;
+
+    /**
+     * The parser to use for creating geodetic objects from WKT definitions.
+     * All uses of this parser shall be synchronized by the <code>{@linkplain 
#lock}.writeLock()</code>.
+     */
+    private final WKTFormat parser;
+
+    /**
+     * The write lock for {@link #parser} and the read/write locks for {@link 
#definitions} accesses.
+     *
+     * <div class="note"><b>Implementation note:</b>
+     * we manage the locks ourselves instead than using a {@link 
java.util.concurrent.ConcurrentHashMap}
+     * because if a {@link #definitions} value needs to be computed, then we 
need to block all other
+     * threads anyway since {@link #parser} is not thread-safe. Consequently 
the high concurrency
+     * capability provided by {@code ConcurrentHashMap} does not help us in 
this case.</div>
+     */
+    private final ReadWriteLock lock;
+
+    /**
+     * CRS definitions associated to <var>authority:version:code</var> keys.
+     * Keys are authority codes, ignoring code space (authority) and version.
+     * For example in "EPSG:9.1:4326" the key would be only "4326".
+     * Values can be one of the following 4 types:
+     *
+     * <ol>
+     *   <li>{@link Element}: 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}
+     *       when {@link #createObject(String)} is invoked for a given 
authority code.
+     *       The parsing result replaces the previous {@link Element} 
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
+     *       in a {@link Disambiguation} object.</li>
+     *   <li>{@link String} if parsing failed, in which case the string is the 
error message.</li>
+     * </ol>
+     *
+     * <h4>Synchronization</h4>
+     * 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 #createObject(String)
+     */
+    private final Map<String,Object> definitions;
+
+    /**
+     * A special kind of value used in the {@link #definitions} map when the 
same code is used by more
+     * than one authority and version. In the common case where a {@link 
WKTDictionary} instance
+     * contains definitions for only one namespace and version, this class 
will never be instantiated.
+     */
+    private static final class Disambiguation {
+        /**
+         * The previous {@code Disambiguation} in a linked list, or {@code 
null} if we reached the end of list.
+         * The use of a linked list should be efficient enough if the amount 
of {@code Disambiguation}s for a
+         * given code is small.
+         */
+        private final Disambiguation previous;
+
+        /**
+         * The authority (or other kind of code space) providing CRS 
definitions.
+         */
+        private final String codespace;
+
+        /**
+         * Version of the CRS definition, or {@code null} if unspecified.
+         */
+        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.
+         */
+        Object value;
+
+        /**
+         * Creates a new {@code Disambiguation} instance as a wrapper around 
the given identifier object.
+         * This constructor may be invoked if {@link WKTDictionary} has been 
used for creating some
+         * objects before new definitions are added. It should rarely happen.
+         *
+         * @param  object  the CRS (or other geodetic object) to wrap.
+         */
+        Disambiguation(final IdentifiedObject object) {
+            /*
+             * Identifier should never be null because `WKTDictionary` accepts 
only definitions having
+             * an `ID[…]` or `AUTHORITY[…]` element. A WKT can contain at most 
one of those elements.
+             */
+            final Identifier id = 
CollectionsExt.first(object.getIdentifiers());
+            codespace = id.getCodeSpace();
+            version   = id.getVersion();
+            value     = object;
+            previous  = null;
+        }
+
+        /**
+         * Creates a new {@code Disambiguation} instance as a wrapper around 
the given identifier object.
+         *
+         * @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) {
+            Arrays.fill(fullId, null);
+            
object.peekLastElement(MathTransformParser.ID_KEYWORDS).peekValues(fullId);
+            codespace = trimOrNull(fullId[0]);
+            version   = trimOrNull(fullId[2]);
+            value     = object;
+            previous  = null;
+        }
+
+        /**
+         * Creates a new {@code Disambiguation} instance identified by {@code 
codespace:version:code}.
+         *
+         * @param  previous   previous disambiguation, or {@code null} if none.
+         * @param  codespace  the authority (or other kind of code space) 
providing CRS definitions.
+         * @param  version    version of the CRS definition, or {@code null} 
if unspecified.
+         * @param  code       code allocated by the authority for the CRS 
definition.
+         * @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)
+         */
+        Disambiguation(Disambiguation previous, final String codespace, final 
String version,
+                       final String code, final Element value)
+        {
+            this.previous  = previous;
+            this.codespace = codespace;
+            this.version   = version;
+            this.value     = value;
+            while (previous != null) {
+                if (Strings.equalsIgnoreCase(codespace, previous.codespace) &&
+                    Strings.equalsIgnoreCase(version,   previous.version))
+                {
+                    throw new IllegalArgumentException(Errors.format(
+                            Errors.Keys.DuplicatedIdentifier_1, 
identifier(code)));
+                }
+                previous = previous.previous;
+            }
+        }
+
+        /**
+         * Finds the {@code Disambiguation} for the given authority and 
version.
+         *
+         * @param  choices    end of a linked list of {@code Disambiguation}s, 
or {@code null} if none.
+         * @param  codespace  the authority providing CRS definitions, or 
{@code null} if unspecified.
+         * @param  version    version of the CRS definition, or {@code null} 
if unspecified.
+         * @param  code       code allocated by the authority for the CRS 
definition.
+         * @return container for the given authority and version, or {@code 
null} if none.
+         * @throws NoSuchAuthorityCodeException if the given authority and 
version are ambiguous.
+         */
+        static Disambiguation find(Disambiguation choices, final String 
codespace, final String version, final String code)
+                throws NoSuchAuthorityCodeException
+        {
+            Disambiguation found = null;
+            for (boolean isExact = false; choices != null; choices = 
choices.previous) {
+                if (codespace == null || 
codespace.equalsIgnoreCase(choices.codespace)) {
+                    if (Strings.equalsIgnoreCase(version, choices.version)) {
+                        if (!isExact) {
+                            isExact = true;
+                            found = choices;        // Silently discard 
previous value since we have a better match.
+                            continue;
+                        }
+                    } else if (isExact) {
+                        continue;                   // Ignore this value since 
previous one was a better match.
+                    }
+                    if (isExact && found != null) {
+                        final String identifier = identifier(codespace, 
version, code);
+                        throw new 
NoSuchAuthorityCodeException(Errors.format(Errors.Keys.AmbiguousName_3,
+                                choices.identifier(code), 
found.identifier(code), identifier),
+                                codespace, code, identifier);
+                    }
+                    found = choices;
+                }
+            }
+            return found;
+        }
+
+        /**
+         * Adds all authority codes to the given set.
+         *
+         * @param  choices  end of a linked list of {@code Disambiguation}s.
+         * @param  code     authority code (code space and version may vary).
+         * @param  addTo    where to add the {@code codespace:version:code} 
tuples.
+         *
+         * @see WKTDictionary#getAuthorityCodes(Class)
+         */
+        static void list(Disambiguation choices, final String code, final 
Set<String> addTo) {
+            do {
+                addTo.add(choices.identifier(code));
+                choices = choices.previous;
+            } while (choices != null);
+        }
+
+        /**
+         * Creates an <var>authority:version:code</var> identifier with the 
given code.
+         * This is used for formatting error messages.
+         */
+        private String identifier(final String code) {
+            return identifier(codespace, version, code);
+        }
+
+        /**
+         * Creates an <var>authority:version:code</var> identifier with the 
given code.
+         * This is used for formatting error messages.
+         */
+        private static String identifier(final String codespace, final String 
version, final String code) {
+            return Strings.orEmpty(codespace) + Constants.DEFAULT_SEPARATOR +
+                   Strings.orEmpty(version)   + Constants.DEFAULT_SEPARATOR +
+                   Strings.orEmpty(code);
+        }
+    }
+
+    /**
+     * Creates an initially empty factory. The authority can specified 
explicitly or inferred from the WKTs.
+     * In the later case (when the given authority is {@code null}), an 
authority will be inferred from all
+     * {@code ID[…]} or {@code AUTHORITY[…]} elements found in WKT strings as 
below, in preference order:
+     *
+     * <ol>
+     *   <li>Most frequent {@code CITATION[…]} value.</li>
+     *   <li>If there is no citation, then most frequent code space
+     *       in {@code ID[…]} or {@code AUTHORITY[…]} elements.</li>
+     * </ol>
+     *
+     * The WKT strings are specified by calls to {@link #load(BufferedReader)} 
or {@link #addDefinitions(Stream)}
+     * after construction.
+     *
+     * @param  authority  organization that defines the codes recognized by 
this factory, or {@code null}.
+     */
+    public WKTDictionary(final Citation authority) {
+        /*
+         * Note: we do not allow users to specify their own `WKTFormat` 
instance because current
+         * `WKTDictionary` implementation invokes package-private methods. If 
user supplies
+         * a `WKTFormat` with overridden public methods, (s)he may be 
surprised to see that those
+         * methods are not invoked.
+         */
+        definitions = new HashMap<>();
+        codespaces  = new FrequencySortedSet<>(true);
+        parser      = new WKTFormat(null, null);
+        lock        = new ReentrantReadWriteLock();
+        authorities = (authority != null) ? null : new 
FrequencySortedSet<>(true);
+        this.authority = authority;
+    }
+
+    /**
+     * If {@link #authority} is not yet defined, computes a value from {@code 
ID[…]} found
+     * in all WKT strings. This method should be invoked after new WKTs have 
been added.
+     */
+    private void updateAuthority() {
+        if (authorities != null) {
+            String name = CollectionsExt.first(authorities);        // Most 
frequently declared authority.
+            if (name == null) {
+                name = CollectionsExt.first(codespaces);            // Most 
frequently declared codespace.
+            }
+            authority = Citations.fromName(name);                   // May 
still be null.
+        }
+    }
+
+    /**
+     * Keyword recognized by {@link #load(BufferedReader)}.
+     */
+    private static final String SET = "SET";
+
+    /**
+     * Adds to this factory all definitions read from the given source.
+     * Each Coordinate Reference System (or other geodetic object) is defined 
by a string in WKT format.
+     * The key associated to each object is given by the {@code ID[…]} or 
{@code AUTHORITY[…]} element,
+     * which is typically the last element of a WKT string and is mandatory 
for definitions in this file.
+     *
+     * <p>WKT strings can span many lines. All lines after the first line 
shall be indented with at least
+     * one white space. Non-indented lines start new definitions.</p>
+     *
+     * <p>Blank lines and lines starting with the {@code #} character 
(ignoring white spaces) are ignored.</p>
+     *
+     * <h4>Aliases for WKT fragments</h4>
+     * Files with more than one WKT definition tend to repeat the same WKT 
fragments many times.
+     * For example the same {@code BaseGeogCRS[…]} element may be repeated in 
every {@code ProjectedCRS} definitions.
+     * Redundant fragments can be replaced by aliases for making the file more 
compact,
+     * easier to read, faster to parse and with smaller memory footprint.
+     *
+     * <p>Each line starting with "<code>SET 
&lt;<var>identifier</var>&gt;=&lt;<var>WKT</var>&gt;</code>"
+     * defines an alias for a fragment of WKT string. The WKT can span many 
lines as described above.
+     * Aliases are local to the file where they are defined.
+     * Aliases can be expanded in other WKT strings by 
"<code>$&lt;<var>identifier</var>&gt;</code>".</p>
+     *
+     * <h4>Validation</h4>
+     * This method verifies that definitions have matching quotes, balanced 
parenthesis or brackets,
+     * and valid number or date formats. It does not verify WKT keywords or 
geodetic parameters.
+     * See class javadoc for more details.
+     *
+     * <h4>Example</h4>
+     * An example is <a href="./doc-files/ESRI.txt">available here</a>.
+     *
+     * @param  source  the source of WKT definitions.
+     * @throws FactoryException if the definition file can not be read.
+     */
+    public void load(final BufferedReader source) throws FactoryException {
+        ArgumentChecks.ensureNonNull("source", source);
+        lock.writeLock().lock();
+        try {
+            final Loader loader = new Loader(source);
+            try {
+                loader.read();
+            } catch (IOException e) {
+                throw new FactoryException(loader.canNotRead(null, e), e);
+            } catch (ParseException | IllegalArgumentException e) {
+                throw new FactoryDataException(loader.canNotRead(null, e), e);
+            } finally {
+                loader.restore();
+                updateAuthority();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * Implementation of {@link WKTDictionary#load(BufferedReader)} method.
+     * Caller must own the write lock before to instantiate and use this class.
+     */
+    private final class Loader {
+        /** The source of WKT definitions. */
+        private final BufferedReader source;
+
+        /** Temporary buffer where to put the WKT to parse. */
+        private final StringBuilder buffer;
+
+        /** If the WKT being parsed is an alias, the alias key. Otherwise 
{@code null}. */
+        private String aliasKey;
+
+        /** Argument for {@link #addAliasOrDefinition()}. */
+        private final ParsePosition pos;
+
+        /** Zero-based number of current line. Equivalent to {@link 
LineNumberReader#getLineNumber()}. */
+        private int lineNumber;
+
+        /** Aliases that existed before in {@link #parser} before loading 
started. */
+        private final Set<String> aliases;
+
+        /** Creates a new loader. */
+        Loader(final BufferedReader source) {
+            this.source = source;
+            buffer  = new StringBuilder(500);
+            pos     = new ParsePosition(0);
+            aliases = new HashSet<>(parser.getFragmentNames());
+        }
+
+        /**
+         * Restores {@link #parser} to its initial state. This method should 
be invoked
+         * in a finally block regardless if the parsing succeeded or failed.
+         */
+        final void restore() {
+            parser.getFragmentNames().retainAll(aliases);
+            parser.clear();
+        }
+
+        /**
+         * Returns an error message saying "Can not read WKT at line X". The 
message is followed
+         * by a "Caused by" phrase specified either as a string or an 
exception. At least one of
+         * {@code cause} and {@code e} shall be non-null.
+         */
+        final String canNotRead(String cause, final Exception e) {
+            final Locale locale = parser.getErrorLocale();
+            if (cause == null) {
+                cause = Exceptions.getLocalizedMessage(e, locale);
+            }
+            return 
Resources.forLocale(locale).getString(Resources.Keys.CanNotParseWKT_2, 
getLineNumber(), cause);
+        }
+
+        /**
+         * Returns the one-based line number of the last line read.
+         * Actually this method returns the zero-based line number of current 
position,
+         * but since current position is after the last line read, this is 
equivalent
+         * to line number of last line read + 1.
+         *
+         * @return one-based line number of current position.
+         */
+        private int getLineNumber() {
+            if (source instanceof LineNumberReader) {
+                // In case an unusual implementation counts lines in a 
different way than we do.
+                lineNumber = ((LineNumberReader) source).getLineNumber();
+            }
+            return lineNumber;
+        }
+
+        /**
+         * Adds to the enclosing factory all definitions read from the given 
source.
+         * See {@link WKTDictionary#load(BufferedReader)} for a format 
description.
+         *
+         * @throws IOException if an error occurred while reading lines.
+         * @throws ParseException if an error occurred while parsing a WKT.
+         * @throws FactoryDataException if the file has a syntax error.
+         * @throws IllegalArgumentException if a {@code 
codespace:version:code} tuple or an alias is assigned twice.
+         */
+        final void read() throws IOException, ParseException, 
FactoryDataException {
+            final String lineSeparator = System.lineSeparator();
+            int indentation = 0;
+            String line;
+            while ((line = source.readLine()) != null) {
+                lineNumber++;
+                final int length = line.length();
+                int defStart = CharSequences.skipLeadingWhitespaces(line, 0, 
length);
+                if (defStart < length && line.charAt(defStart) == '#') 
continue;        // Skip comment lines.
+                /*
+                 * If the line is indented compared to the first line, we 
presume that it is the continuation
+                 * of previous line and skip the check for "SET" keyword. If 
the line is not indented,
+                 * previous buffer content need to be parsed before we start a 
new WKT definition.
+                 */
+                if (defStart > indentation) {
+                    defStart = indentation;
+                } else {
+                    addAliasOrDefinition();
+                    indentation = defStart;
+                    if (line.regionMatches(true, defStart, SET, 0, 
SET.length())) {
+                        final int keyStart = 
CharSequences.skipLeadingWhitespaces(line, defStart + SET.length(), length);
+                        if (keyStart > defStart) {             // `true` if 
"SET" is followed by at least one white space.
+                            defStart = line.indexOf('=', keyStart);
+                            if (defStart <= keyStart) {
+                                throw new 
FactoryDataException(resources().getString(
+                                            
Resources.Keys.SyntaxErrorForAlias_1, getLineNumber()));
+                            }
+                            final int keyEnd = 
CharSequences.skipTrailingWhitespaces(line, keyStart, defStart);
+                            defStart = 
CharSequences.skipLeadingWhitespaces(line, defStart + 1, length);
+                            final String key = line.substring(keyStart, 
keyEnd);
+                            if (!CharSequences.isUnicodeIdentifier(key)) {
+                                String c = 
parser.errors().getString(Errors.Keys.NotAUnicodeIdentifier_1, key);
+                                throw new FactoryDataException(canNotRead(c, 
null));
+                            }
+                            aliasKey = key;
+                        }
+                    }
+                }
+                /*
+                 * Copy non-empty lines in the buffer, omitting indentation 
and trailing spaces.
+                 * The leading spaces after indentation are kept in order to 
have a more readable
+                 * WKT string in error message if parsing fail.
+                 */
+                final int end = CharSequences.skipTrailingWhitespaces(line, 
defStart, length);
+                if (defStart < end) {
+                    if (buffer.length() != 0) buffer.append(lineSeparator);
+                    buffer.append(line, defStart, end);
+                }
+            }
+            addAliasOrDefinition();
+        }
+
+        /**
+         * Parses the current {@link #buffer} content as a WKT elements 
(possibly with children elements).
+         * This method does not build the full {@link IdentifiedObject}; this 
later part will be done only
+         * when first needed.
+         *
+         * <p>If {@link #aliasKey} is non-null, the first WKT is taken as a 
{@linkplain WKTFormat#addFragment
+         * fragment} associated to the given alias. All other WKT (if any) are 
taken as definitions of CRS or
+         * other objects.</p>
+         *
+         * @throws ParseException if an error occurred while parsing the WKT 
string.
+         * @throws FactoryDataException if there is unparsed text after the 
WKT.
+         * @throws IllegalArgumentException if a {@code 
codespace:version:code} tuple or an alias is assigned twice.
+         */
+        private void addAliasOrDefinition() throws ParseException, 
FactoryDataException {
+            if (buffer.length() != 0) {
+                pos.setIndex(0);
+                final String wkt = buffer.toString();
+                final Element root = 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);
+                    aliasKey = null;
+                } else {
+                    addDefinition(root);
+                }
+                buffer.setLength(0);
+            }
+        }
+    }
+
+    /**
+     * Adds the definition of a CRS (or other geodetic objects) from a tree of 
WKT elements.
+     * The authority code is inferred from the {@code ID[…]} or {@code 
AUTHORITY[…]} element.
+     * 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.
+     * @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;
+            }
+        }
+        throw new 
FactoryDataException(resources().getString(Resources.Keys.MissingAuthorityCode_1,
 root.peekValue()));
+    }
+
+    /**
+     * Adds definitions of CRS (or other geodetic objects) from Well-Known 
Texts. Blank strings are ignored.
+     * Each non-blank {@link String} shall contain the complete definition of 
at least one geodetic object.
+     * More than one geodetic object can appear in the same {@link String} if 
they are separated by spaces
+     * or line separators. However the same geodetic object can not have its 
definition splitted in two or
+     * more {@link String}s.
+     *
+     * <p>The key associated to each object is given by the {@code ID[…]} or 
{@code AUTHORITY[…]} element,
+     * which is typically the last element of a WKT string and is mandatory. 
WKT strings can contain line
+     * separators for human readability.</p>
+     *
+     * @param  objects  CRS (or other geodetic objects) definitions as WKT 
strings.
+     * @throws FactoryException if a WKT can not be parsed, or does not 
contain an {@code ID[…]} or
+     *         {@code AUTHORITY[…]} element, or if the same {@code 
codespace:version:code} tuple is
+     *         used for two objects.
+     */
+    public void addDefinitions(final Stream<String> objects) throws 
FactoryException {
+        ArgumentChecks.ensureNonNull("objects", objects);
+        /*
+         * We work with iterator because we do not support parallelism yet.
+         * However a future version may support that, which is why argument
+         * type is a `Stream`.
+         */
+        final Iterator<String> it = objects.iterator();
+        final ParsePosition pos = new ParsePosition(0);
+        lock.writeLock().lock();
+        try {
+            int lineNumber = 1;
+            try {
+                while (it.hasNext()) {
+                    final String wkt = it.next();
+                    final int length = 
CharSequences.skipTrailingWhitespaces(wkt, 0, wkt.length());
+                    while (pos.getIndex() < length) {
+                        addDefinition(parser.textToTree(wkt, pos));
+                    }
+                    pos.setIndex(0);
+                    lineNumber++;
+                }
+            } catch (ParseException | IllegalArgumentException e) {
+                throw new FactoryDataException(resources().getString(
+                        Resources.Keys.CanNotParseWKT_2, lineNumber, 
e.getLocalizedMessage()));
+            } finally {
+                updateAuthority();
+            }
+        } finally {
+            lock.writeLock().unlock();
+        }
+    }
+
+    /**
+     * Convenience methods for resources in the language used for error 
messages.
+     */
+    private Resources resources() {
+        return Resources.forLocale(parser.getErrorLocale());
+    }
+
+    /**
+     * Trims the leading and trailing spaces of the string representation of 
given object.
+     * If null, empty or contains only spaces, then this method returns {@code 
null}.
+     */
+    private static String trimOrNull(final Object value) {
+        return (value != null) ? Strings.trimOrNull(value.toString()) : null;
+    }
+
+    /**
+     * Returns the authority or specification that defines the codes 
recognized by this factory.
+     * This is the first of the following values, in preference order:
+     *
+     * <ol>
+     *   <li>The authority explicitly specified at construction time.</li>
+     *   <li>A citation built from the most frequent value found in {@code 
CITATION} elements.</li>
+     *   <li>A citation built from the most frequent value found in {@code ID} 
or {@code AUTHORITY} elements.</li>
+     * </ol>
+     *
+     * @return the organization responsible for CRS definitions, or {@code 
null} if unknown.
+     */
+    @Override
+    public Citation getAuthority() {
+        return authority;
+    }
+
+    /**
+     * Returns all namespaces recognized by this factory. Those namespaces can 
appear before codes in
+     * calls to {@code createFoo(String)} methods, for example {@code "ESRI"} 
in {@code "ESRI:102018"}.
+     * Namespaces are case-insensitive.
+     *
+     * @return the namespaces recognized by this factory.
+     */
+    @Override
+    public Set<String> getCodeSpaces() {
+        lock.readLock().lock();
+        try {
+            return JDK9.copyOf(codespaces);
+        } finally {
+            lock.readLock().unlock();
+        }
+    }
+
+    /**
+     * Returns an arbitrary object from a code.
+     *
+     * @param  code  value allocated by authority.
+     * @return the object for the given code.
+     * @throws NoSuchAuthorityCodeException if the specified {@code code} was 
not found.
+     * @throws FactoryException if the object creation failed for some other 
reason.
+     */
+    @Override
+    public IdentifiedObject createObject(final String code) throws 
FactoryException {
+        /*
+         * Separate the authority from the rest of the code. The 
CharSequences.skipWhitespaces(…)
+         * methods are robust to negative index and will work even if 
code.indexOf(…) returned -1.
+         */
+        String codespace = null;
+        String version   = null;
+        String localCode = code;
+        int afterAuthority = code.indexOf(Constants.DEFAULT_SEPARATOR);
+        int end = CharSequences.skipTrailingWhitespaces(code, 0, 
afterAuthority);
+        int start = CharSequences.skipLeadingWhitespaces(code, 0, end);
+        if (start < end) {
+            codespace = code.substring(start, end);
+            /*
+             * Separate the version from the rest of the code. The version is 
optional. The code may have no room
+             * for version (e.g. "EPSG:4326"), or specify an empty version 
(e.g. "EPSG::4326"). If the version is
+             * equals to an empty string, it will be considered as no version.
+             */
+            int afterVersion = code.indexOf(Constants.DEFAULT_SEPARATOR, 
++afterAuthority);
+            start = CharSequences.skipLeadingWhitespaces(code, afterAuthority, 
afterVersion);
+            end = CharSequences.skipTrailingWhitespaces(code, start, 
afterVersion++);
+            if (start < end) {
+                version = code.substring(start, end);
+            }
+            start = Math.max(afterAuthority, afterVersion);
+            end = code.length();
+            localCode = CharSequences.trimWhitespaces(code, start, 
end).toString();
+        }
+        /*
+         * 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`).
+         */
+        Disambiguation choices = null;
+        Object value = null;
+        lock.readLock().lock();
+        try {
+            boolean valid = (codespace == null || codespace.isEmpty() || 
codespaces.contains(codespace));
+            if (!valid) {
+                for (final String cs : codespaces) {            // More costly 
check if no exact match.
+                    valid = cs.equalsIgnoreCase(codespace);
+                    if (valid) break;
+                }
+            }
+            if (valid) {
+                value = definitions.get(localCode);
+                if (value instanceof Disambiguation) {
+                    choices = Disambiguation.find((Disambiguation) value, 
codespace, version, localCode);
+                    value = (choices != null) ? choices.value : null;
+                }
+            }
+        } finally {
+            lock.readLock().unlock();
+        }
+        if (value == null) {
+            throw new NoSuchAuthorityCodeException(parser.errors().getString(
+                    Errors.Keys.NoSuchValue_1, code), codespace, localCode, 
code);
+        }
+        /*
+         * 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.
+         *   - `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).
+         * Must be done under write lock because `parser` is not thread-safe.
+         */
+        if (value instanceof Element) {
+            lock.writeLock().lock();
+            try {
+                if (choices != null) {
+                    value = choices.value;              // Check again in case 
value has been computed concurrently.
+                } else {
+                    value = definitions.get(localCode);
+                }
+                if (value instanceof Element) {
+                    ParseException cause = null;
+                    try {
+                        value = parser.parse((Element) value);
+                    } catch (ParseException e) {
+                        cause = e;
+                        value = e.getLocalizedMessage();
+                        if (value == null) {
+                            value = e.getClass().getSimpleName();
+                        }
+                    }
+                    if (choices != null) {
+                        choices.value = value;          // Save result for 
future uses.
+                    } else {
+                        definitions.put(localCode, value);
+                    }
+                    if (cause != null) {
+                        throw new FactoryException(resources().getString(
+                                
Resources.Keys.CanNotInstantiateGeodeticObject_1, code), cause);
+                    }
+                }
+            } finally {
+                lock.writeLock().unlock();
+            }
+        }
+        if (value instanceof IdentifiedObject) {
+            return (IdentifiedObject) value;
+        } else {
+            // Exception message saved in a previous invocation of this method.
+            throw new FactoryException(String.valueOf(value));
+        }
+    }
+
+    /**
+     * Returns the set of authority codes for objects of the given type.
+     * The {@code type} argument specifies the base type of identified objects.
+     *
+     * @param  type  the spatial reference objects type.
+     * @return the set of authority codes for spatial reference objects of the 
given type.
+     * @throws FactoryException if an error occurred while fetching the codes.
+     */
+    @Override
+    public Set<String> getAuthorityCodes(Class<? extends IdentifiedObject> 
type) throws FactoryException {
+        final Set<String> codes = new 
HashSet<>(Containers.hashMapCapacity(definitions.size()));
+        for (final Map.Entry<String,Object> entry : definitions.entrySet()) {
+            final String code  = entry.getKey();
+            final Object value = entry.getValue();
+            if (value instanceof Disambiguation) {
+                Disambiguation.list((Disambiguation) value, code, codes);
+            } else {
+                codes.add(code);
+            }
+        }
+        return 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 f9ab3e9..6b03397 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
@@ -340,7 +340,7 @@ public class WKTFormat extends CompoundFormat<Object> {
      *
      * @see #errors()
      */
-    private Locale getErrorLocale() {
+    final Locale getErrorLocale() {
         final Locale locale = getLocale(Locale.Category.DISPLAY);
         return (locale != null && locale != Locale.ROOT) ? locale : 
Locale.getDefault(Locale.Category.DISPLAY);
     }
@@ -757,22 +757,32 @@ public class WKTFormat extends CompoundFormat<Object> {
     public void addFragment(final String name, final String wkt) throws 
IllegalArgumentException, ParseException {
         ArgumentChecks.ensureNonEmpty("wkt", wkt);
         ArgumentChecks.ensureNonEmpty("name", name);
-        short error = Errors.Keys.NotAUnicodeIdentifier_1;
-        if (CharSequences.isUnicodeIdentifier(name)) {
-            final ParsePosition pos = new ParsePosition(0);
-            final Element element = parseFragment(wkt, pos);
-            final int index = CharSequences.skipLeadingWhitespaces(wkt, 
pos.getIndex(), wkt.length());
-            if (index < wkt.length()) {
-                throw new UnparsableObjectException(getErrorLocale(), 
Errors.Keys.UnexpectedCharactersAfter_2,
-                        new Object[] {name + " = " + element.keyword + "[…]", 
CharSequences.token(wkt, index)}, index);
-            }
-            // `fragments` map has been created by `parser(true)`.
-            if (fragments.putIfAbsent(name, element) == null) {
-                return;
-            }
-            error = Errors.Keys.ElementAlreadyPresent_1;
+        if (!CharSequences.isUnicodeIdentifier(name)) {
+            throw new 
IllegalArgumentException(errors().getString(Errors.Keys.NotAUnicodeIdentifier_1,
 name));
+        }
+        final ParsePosition pos = new ParsePosition(0);
+        final Element element = 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);
+        }
+        addFragment(name, element);
+    }
+
+    /**
+     * 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.
+     * @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) {
+            throw new 
IllegalArgumentException(errors().getString(Errors.Keys.ElementAlreadyPresent_1,
 name));
         }
-        throw new IllegalArgumentException(errors().getString(error, name));
     }
 
     /**
@@ -782,11 +792,19 @@ public class WKTFormat extends CompoundFormat<Object> {
      * @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 parseFragment(final String wkt, final ParsePosition pos) 
throws ParseException {
+    final Element textToTree(final String wkt, final ParsePosition pos) throws 
ParseException {
         if (sharedValues == null) {
             sharedValues = new HashMap<>();
         }
-        return new Element(parser(true), wkt, pos, sharedValues);
+        return parser(true).textToTree(wkt, pos, sharedValues);
+    }
+
+    /**
+     * Clears warnings and cache of shared values.
+     */
+    final void clear() {
+        warnings = null;
+        sharedValues = null;
     }
 
     /**
@@ -802,17 +820,36 @@ public class WKTFormat extends CompoundFormat<Object> {
      */
     @Override
     public Object parse(final CharSequence wkt, final ParsePosition pos) 
throws ParseException {
-        warnings = null;
-        sharedValues = null;
+        clear();
         ArgumentChecks.ensureNonEmpty("wkt", wkt);
         ArgumentChecks.ensureNonNull ("pos", pos);
         final AbstractParser parser = parser(false);
         Object object = null;
         try {
-            return object = parser.parseObject(wkt.toString(), pos);
+            object = parser.parseObject(wkt.toString(), pos);
         } finally {
             warnings = parser.getAndClearWarnings(object);
         }
+        return object;
+    }
+
+    /**
+     * Creates an object from the given tree of WKT elements.
+     *
+     * @param  root  the tree of WKT elements.
+     * @return the parsed object (never {@code null}).
+     * @throws ParseException if an error occurred while parsing the WKT.
+     */
+    final Object parse(final Element root) throws ParseException {
+        clear();
+        final AbstractParser parser = parser(false);
+        Object object = null;
+        try {
+            object = parser.buildFromTree(root);
+        } finally {
+            warnings = parser.getAndClearWarnings(object);
+        }
+        return object;
     }
 
     /**
@@ -822,6 +859,10 @@ public class WKTFormat extends CompoundFormat<Object> {
      */
     private AbstractParser parser(final boolean modifiable) {
         AbstractParser parser = this.parser;
+        /*
+         * `parser` is always null on a fresh clone. However the `fragments`
+         * map may need to be cloned if the caller intents to modify it.
+         */
         if (parser == null || (isCloned & modifiable)) {
             this.parser = parser = new Parser(symbols, fragments(modifiable),
                     (NumberFormat) getFormat(Number.class),
@@ -871,7 +912,7 @@ public class WKTFormat extends CompoundFormat<Object> {
      */
     @Override
     public void format(final Object object, final Appendable toAppendTo) 
throws IOException {
-        warnings = null;
+        clear();
         ArgumentChecks.ensureNonNull("object",     object);
         ArgumentChecks.ensureNonNull("toAppendTo", toAppendTo);
         /*
@@ -970,7 +1011,7 @@ public class WKTFormat extends CompoundFormat<Object> {
     /**
      * Convenience methods for resources for error message in the locale given 
by {@link #getLocale()}.
      */
-    private Errors errors() {
+    final Errors errors() {
         return Errors.getResources(getErrorLocale());
     }
 
@@ -983,11 +1024,10 @@ public class WKTFormat extends CompoundFormat<Object> {
     @Override
     public WKTFormat clone() {
         final WKTFormat clone = (WKTFormat) super.clone();
-        clone.sharedValues = null;
+        clone.clear();
         clone.factories    = null;                              // Not 
thread-safe; clone needs its own.
         clone.formatter    = null;                              // Do not 
share the formatter.
         clone.parser       = null;
-        clone.warnings     = null;
         clone.isCloned = isCloned = true;
         // Symbols and Colors do not need to be cloned because they are 
flagged as immutable.
         return clone;
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
new file mode 100644
index 0000000..c1b56f3
--- /dev/null
+++ 
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/doc-files/ESRI.txt
@@ -0,0 +1,69 @@
+#
+# Example file for org.apache.sis.io.wkt.ReferencingFactory.
+# This file provides Coordinate Reference System definitions
+# in Well-Known Text (WKT) format. One or many files can be
+# used for extending the set of CRS with custom definitions.
+#
+# Each Coordinate Reference System (or other geodetic object)
+# is defined by a string in WKT format. The key associated to
+# each object is given by the ID[…] or AUTHORITY[…] element,
+# which is typically the last element of a WKT string and is
+# mandatory for definitions in this file.
+#
+# WKT strings can span many lines. All lines after the first
+# line shall be indented with at least one white space.
+# Non-indented lines start new definitions.
+#
+# All lines starting with the # character are comment lines.
+#
+# ---- Aliases for WKT fragments ---------------------------
+# Files with more than one WKT definition tend to repeat the
+# same WKT fragments many times, e.g. the same BaseGeogCRS[…]
+# element may be repeated in every ProjectedCRS definitions.
+# Redundant fragments can be replaced by aliases for making
+# the file more compact, easier to read, faster to parse and
+# with smaller memory footprint.
+#
+# Each line starting with "SET <identifier>=<WKT>" defines
+# an alias for a fragment of WKT string.  The WKT can span
+# many lines as described above.  Aliases are local to the
+# file where they are defined.  Aliases can be expanded in
+# other WKT strings by "$<identifier>".
+#
+
+#
+# Alias for WGS84 geographic CRS.
+# Can be inserted in projected CRS with "$WGS84".
+#
+SET WGS84 =
+ BaseGeodCRS["GCS_WGS_1984",
+  Datum["D_WGS_1984",
+   Ellipsoid["WGS_1984", 6378137, 298.257223563]],
+  AngleUnit["Degree", 0.0174532925199433]]
+
+#
+# Derived from https://github.com/Esri/projection-engine-db-doc
+# with base CRS replaced by alias and parameter values omitted
+# when they have the default value.
+#
+ProjectedCRS["North_Pole_Stereographic",
+ $WGS84,
+ Conversion["Stereographic North Pole",
+  Method["Polar Stereographic (variant A)"],
+  Parameter["Latitude of natural origin", 90]],
+ CS[Cartesian, 2],
+  Axis["Easting (E)", east],
+  Axis["Northing (N)", north],
+  Unit["metre", 1],
+ Id["ESRI", 102018]]
+
+ProjectedCRS["South_Pole_Stereographic",
+ $WGS84,
+ Conversion["Stereographic South Pole",
+  Method["Polar Stereographic (variant A)"],
+  Parameter["Latitude of natural origin", -90]],
+ CS[Cartesian, 2],
+  Axis["Easting (E)", east],
+  Axis["Northing (N)", north],
+  Unit["metre", 1],
+ Id["ESRI", 102021]]
diff --git 
a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java 
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java
index 2f21e4c..169fa18 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/package-info.java
@@ -64,7 +64,7 @@
  * The {@link org.apache.sis.geometry.GeneralEnvelope} and {@link 
org.apache.sis.geometry.GeneralDirectPosition} classes
  * provide their own, limited, WKT parsing and formatting services for the 
{@code BOX} and {@code POINT} elements.
  * A description for this WKT format can be found on
- * <a 
href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_coordinate_reference_systems";>Wikipedia</a>.
+ * <a 
href="https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry";>Wikipedia</a>.
  *
  * <h2>Where to find WKT examples</h2>
  * An excellent source of well-formed WKT is the online <cite>EPSG Geodetic 
Parameter Registry</cite>.
@@ -83,7 +83,7 @@
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Rémi Eve (IRD)
  * @author  Rueben Schulz (UBC)
- * @version 1.0
+ * @version 1.1
  *
  * @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html";>WKT 
2 specification</a>
  * @see <a 
href="http://www.geoapi.org/3.0/javadoc/org/opengis/referencing/doc-files/WKT.html";>Legacy
 WKT 1</a>
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
new file mode 100644
index 0000000..1e32651
--- /dev/null
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.io.BufferedReader;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.crs.ProjectedCRS;
+import org.opengis.referencing.crs.GeographicCRS;
+import org.opengis.referencing.cs.AxisDirection;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.test.DependsOn;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.apache.sis.test.Assert.*;
+
+
+/**
+ * Tests {@link WKTDictionary}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+@DependsOn(WKTFormatTest.class)
+public final strictfp class WKTDictionaryTest extends TestCase {
+    /**
+     * Tests {@link WKTDictionary#load(BufferedReader)}.
+     *
+     * @throws IOException if an error occurred while reading the test file.
+     * @throws FactoryException if an error occurred while parsing a WKT.
+     */
+    @Test
+    public void testLoad() throws IOException, FactoryException {
+        final WKTDictionary factory = new WKTDictionary(null);
+        try (BufferedReader source = new BufferedReader(new InputStreamReader(
+                WKTFormatTest.class.getResourceAsStream("ExtraCRS.txt"), 
"UTF-8")))
+        {
+            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.
+         */
+        assertArrayEquals("getCodeSpaces()", new String[] {"ESRI", 
"MyCodeSpace"}, factory.getCodeSpaces().toArray());
+        assertSame("getAuthority()", Citations.ESRI, factory.getAuthority());
+        assertSetEquals(Arrays.asList("102018", "ESRI::102021", 
"MyCodeSpace::102021", "MyCodeSpace:v2:102021"),
+                factory.getAuthorityCodes(SingleCRS.class));
+        /*
+         * 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)");
+    }
+
+    /**
+     * Verifies a projected CRS.
+     *
+     * @param crs   the CRS to verify.
+     * @param name  expected CRS name.
+     * @param φ0    expected latitude of origin.
+     */
+    private static void verifyCRS(final ProjectedCRS crs, final String name, 
final double φ0) {
+        assertEquals("name", name, crs.getName().getCode());
+        assertAxisDirectionsEqual(name, crs.getCoordinateSystem(),
+                                  AxisDirection.EAST, AxisDirection.NORTH);
+        assertEquals("φ0", φ0, crs.getConversionFromBase().getParameterValues()
+                                  .parameter("Latitude of natural 
origin").doubleValue(), STRICT);
+    }
+
+    /**
+     * Verifies a geographic CRS.
+     *
+     * @param crs   the CRS to verify.
+     * @param name  expected CRS name.
+     */
+    private static void verifyCRS(final GeographicCRS crs, final String name) {
+        assertEquals("name", name, crs.getName().getCode());
+        assertAxisDirectionsEqual(name, crs.getCoordinateSystem(),
+                                  AxisDirection.NORTH, AxisDirection.EAST);
+    }
+}
diff --git 
a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
 
b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
index ac64c98..074d59f 100644
--- 
a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
+++ 
b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
@@ -222,6 +222,7 @@ import org.junit.BeforeClass;
     org.apache.sis.io.wkt.GeodeticObjectParserTest.class,
     org.apache.sis.io.wkt.WKTFormatTest.class,
     org.apache.sis.io.wkt.WKTParserTest.class,
+    org.apache.sis.io.wkt.WKTDictionaryTest.class,
     org.apache.sis.io.wkt.ComparisonWithEPSG.class,
 
     // Geodetic object creations from authority codes.
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
new file mode 100644
index 0000000..06e7eb9
--- /dev/null
+++ b/core/sis-referencing/src/test/resources/org/apache/sis/io/wkt/ExtraCRS.txt
@@ -0,0 +1,65 @@
+#
+# Test file for org.apache.sis.io.wkt.ReferencingFactory.
+# See "doc-files/ESRI.txt" for file syntax.
+#
+
+#
+# Alias for WGS84 geographic CRS.
+#
+SET DEGREE = Unit["Degree", 0.0174532925199433]
+SET WGS84 =
+ BaseGeodCRS["GCS_WGS_1984",
+  Datum["D_WGS_1984",
+   Ellipsoid["WGS_1984", 6378137, 298.257223563]],
+  $DEGREE]
+
+
+#
+# Derived from https://github.com/Esri/projection-engine-db-doc
+# with base CRS replaced by alias and parameter values omitted
+# when they have the default value.
+#
+ProjectedCRS["North_Pole_Stereographic",
+ $WGS84,
+ Conversion["Stereographic North Pole",
+  Method["Polar Stereographic (variant A)"],
+  Parameter["Latitude of natural origin", 90]],
+ CS[Cartesian, 2],
+  Axis["Easting (E)", east],
+  Axis["Northing (N)", north],
+  Unit["metre", 1],
+ Id["ESRI", 102018]]
+
+ProjectedCRS["South_Pole_Stereographic",
+ $WGS84,
+ Conversion["Stereographic South Pole",
+  Method["Polar Stereographic (variant A)"],
+  Parameter["Latitude of natural origin", -90]],
+ CS[Cartesian, 2],
+  Axis["Easting (E)", east],
+  Axis["Northing (N)", north],
+  Unit["metre", 1],
+ Id["ESRI", 102021]]
+
+#
+# A 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]],
+ CS[ellipsoidal, 2],
+  Axis["Latitude", north],
+  Axis["Longitude", east],
+  $DEGREE,
+ Id["MyCodeSpace", 102021]]
+
+GeodCRS["Anguilla 1957 (bis)",
+  Datum["Anguilla 1957",
+   Ellipsoid["Clarke 1880", 6378249.145, 293.465]],
+ CS[ellipsoidal, 2],
+  Axis["Latitude", north],
+  Axis["Longitude", east],
+  $DEGREE,
+ Id["MyCodeSpace", 102021, "v2"]]
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java 
b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
index 054f4f0..fa9c45a 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/jdk9/JDK9.java
@@ -27,6 +27,7 @@ import java.util.Arrays;
 import java.util.Set;
 import java.util.List;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
@@ -100,7 +101,18 @@ public final class JDK9 {
     }
 
     /**
-     * Placeholder for {@code Map.of(...)} (actually a JDK10 method).
+     * Placeholder for {@code Set.copyOf(...)} (actually a JDK10 method).
+     */
+    public static <V> Set<V> copyOf(final Set<V> set) {
+        switch (set.size()) {
+            case 0:  return Collections.emptySet();
+            case 1:  return Collections.singleton(set.iterator().next());
+            default: return new HashSet<>(set);
+        }
+    }
+
+    /**
+     * Placeholder for {@code Map.copyOf(...)} (actually a JDK10 method).
      */
     public static <K,V> Map<K,V> copyOf(final Map<K,V> map) {
         return map.size() < 2 ? CollectionsExt.compact(map) : new 
HashMap<>(map);
diff --git 
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java 
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
index 2acad54..94d8c5d 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Strings.java
@@ -54,6 +54,31 @@ public final class Strings extends Static {
     }
 
     /**
+     * Returns whether the given strings are equal, ignoring case.
+     * This method accepts null arguments.
+     *
+     * @param  a  first string.
+     * @param  b  another string to be compared with {@code a}.
+     * @return whether the given strings are equal, ignoring case.
+     *
+     * @see java.util.Objects#equals(Object, Object)
+     * @see String#equalsIgnoreCase(String)
+     */
+    public static boolean equalsIgnoreCase(final String a, final String b) {
+        return (a == b) || (a != null && a.equalsIgnoreCase(b));
+    }
+
+    /**
+     * Returns the given text is non-null, or the empty string otherwise.
+     *
+     * @param  text  text or null.
+     * @return given text or empty string (never null).
+     */
+    public static String orEmpty(final String text) {
+        return (text != null) ? text : "";
+    }
+
+    /**
      * Trims the leading and trailing spaces of the given string.
      * If the string is null, empty or contains only spaces, then this method 
returns {@code null}.
      *

Reply via email to