blob: fdbf241c8a2091031c247a7c13b6a7a45aa93bcb [file] [log] [blame]
/*
* 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.Map;
import java.util.Set;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.function.Predicate;
import java.util.function.Consumer;
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.util.InternationalString;
import org.opengis.metadata.citation.Citation;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.NoSuchAuthorityCodeException;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.factory.GeodeticAuthorityFactory;
import org.apache.sis.referencing.factory.FactoryDataException;
import org.apache.sis.referencing.privy.ReferencingUtilities;
import org.apache.sis.referencing.privy.WKTKeywords;
import org.apache.sis.referencing.internal.Resources;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Exceptions;
import org.apache.sis.util.SimpleInternationalString;
import org.apache.sis.util.privy.CollectionsExt;
import org.apache.sis.util.privy.Constants;
import org.apache.sis.util.privy.Strings;
import org.apache.sis.metadata.iso.DefaultIdentifier;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.collection.FrequencySortedSet;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.metadata.Identifier;
/**
* 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.
*
* <h2>Sub-classing and instantiation</h2>
* {@linkplain #WKTDictionary(Citation) Newly constructed} {@code WKTDictionary} are initially empty.
* The dictionary can be populated in the following ways:
*
* <ul>
* <li>Invoke {@link #load(BufferedReader)} for reading definitions from file(s).</li>
* <li>Invoke {@link #addDefinitions(Stream)} for providing definitions from an arbitrary source.</li>
* <li>Override {@link #fetchDefinition(DefaultIdentifier)} in a subclass for fetching WKT definitions
* on-the-fly (for example from the {@code "spatial_ref_sys"} table of a spatial database.</li>
* </ul>
*
* Sub-classing may be necessary even if {@code fetchDefinition(…)} is not overridden
* because {@code WKTDictionary} does not implement any of the
* {@link org.opengis.referencing.crs.CRSAuthorityFactory},
* {@link org.opengis.referencing.cs.CSAuthorityFactory} or
* {@link org.opengis.referencing.datum.DatumAuthorityFactory}.
* The choice of interfaces to implement is left to subclasses.
*
* <h3>Example</h3>
* Extend the set of Coordinate Reference Systems recognized
* by {@link org.apache.sis.referencing.CRS#forCode(String)}.
* The additional CRS are defined by Well-Known Text strings in a {@code "MyCRS.txt"} file.
* First step is to create a CRS factory with those definitions:
*
* {@snippet lang="java" :
* public final class MyCRS extends WKTDictionary implements CRSAuthorityFactory {
* MyCRS() throws IOException, FactoryException {
* super(new DefaultCitation("MyAuthority"));
* try (BufferedReader source = Files.newBufferedReader(Path.of("MyCRS.txt"))) {
* load(source);
* }
* }
* }
* }
*
* The second step is to declare this factory in the {@code module-info.java} file
* as a provider of the {@code org.opengis.referencing.crs.CRSAuthorityFactory} service.
* That file shall contain the class name of above {@code MyCRS} class.
*
* <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 number 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.5
* @since 1.1
*/
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 volatile 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.
* Code spaces are sorted with most frequently used space first.
*
* @see #getCodeSpaces()
*/
private final Set<String> codespaces;
/**
* Cache of authority codes computed by {@link #getAuthorityCodes(Class)}.
* This cache can be cleared at any time; values are recomputed when needed.
*/
private final Map<Class<?>, Set<String>> codeCaches;
/**
* The parser to use for creating geodetic objects from WKT definitions.
* Subclasses can modify the {@code WKTFormat} configuration in their constructor,
* but should not use it directly after construction (for thread safety reasons).
*/
protected final WKTFormat parser;
/**
* The write lock for {@link #parser} and the read/write locks for {@link #definitions} accesses.
* All {@link #parser} usages after {@code WKTDictionary} construction shall be synchronized by
* the {@link ReadWriteLock#writeLock()}.
*
* <h4>Implementation note</h4>
* We manage the locks ourselves instead of 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.
*/
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 StoredTree}: this is the initial state when there are 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 {@link StoredTree}
* when {@link #createObject(String)} is invoked for a given authority code.
* The parsing result replaces the previous {@link StoredTree} value.</li>
* <li>{@link Disambiguation}: if the same code is used by two or more authorities or versions,
* then above-cited {@link StoredTree} or {@link IdentifiedObject} alternatives are wrapped
* in a {@link Disambiguation} object.</li>
* <li>{@link String} if parsing failed, in which case the string is the error message.</li>
* </ol>
*
* <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(StoredTree)
* @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 number 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 StoredTree} before parsing or an {@link IdentifiedObject} after parsing.
* They are the kind of types documented in {@link WKTDictionary#definitions}, excluding other
* {@code Disambiguation} instances.
*/
Object value;
/**
* 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.
*/
private 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.
*/
private Disambiguation(final StoredTree object) {
final Object[] fullId = new Object[3];
object.peekIdentifiers(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 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 oldValue previous value for the same code, or {@code null} if none.
* @param newValue the CRS (or other geodetic object) definition.
* @throws IllegalArgumentException if <var>authority:version:code</var> identifier is already used.
*
* @see WKTDictionary#addDefinition(StoredTree)
*/
Disambiguation(final String codespace, final String version, final String code,
final Object oldValue, final Object newValue)
{
this.codespace = codespace;
this.version = version;
this.value = newValue;
if (oldValue instanceof Disambiguation) {
previous = (Disambiguation) oldValue;
} else if (oldValue instanceof StoredTree) {
previous = new Disambiguation((StoredTree) oldValue);
} else if (oldValue instanceof IdentifiedObject) {
previous = new Disambiguation((IdentifiedObject) oldValue);
} else {
previous = null; // Discard previous parsing failure (a `String` instance).
return;
}
Disambiguation check = previous;
do {
if (Strings.equalsIgnoreCase(codespace, check.codespace) &&
Strings.equalsIgnoreCase(version, check.version))
{
throw new IllegalArgumentException(Errors.format(
Errors.Keys.DuplicatedIdentifier_1, identifier(code)));
}
check = check.previous;
} while (check != null);
}
/**
* 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 filter filter to apply of elements to add in the set.
* @param addTo where to add the {@code codespace:version:code} tuples.
*
* @see WKTDictionary#getAuthorityCodes(Class)
*/
static void list(Disambiguation choices, final String code, final Predicate<Object> filter, final Set<String> addTo) {
do {
if (filter.test(choices.value)) {
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 latter 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<>();
codeCaches = new HashMap<>();
codespaces = new FrequencySortedSet<>(true);
parser = new WKTFormat();
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() {
codeCaches.clear();
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.
}
}
/**
* 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 cannot be read.
*/
public void load(final BufferedReader source) throws FactoryException {
Objects.requireNonNull(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 {
/**
* Keyword recognized by {@link WKTDictionary#load(BufferedReader)}.
*/
private static final String SET = "SET";
/** 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 "Cannot 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();
parser.logWarnings(WKTDictionary.class, "load");
}
/**
* Parses the current {@link #buffer} content as a WKT elements (possibly with children elements).
* This method does not build the full {@link IdentifiedObject}; this latter 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 StoredTree tree = parser.textToTree(wkt, pos, aliasKey);
final int end = pos.getIndex();
if (end < wkt.length()) { // Trailing white spaces already removed by `read(…)`.
throw new FactoryDataException(unexpectedText(getLineNumber(), wkt, end));
}
if (aliasKey != null) {
parser.addFragment(aliasKey, tree);
aliasKey = null;
} else {
addDefinition(tree);
}
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 tree a tree of WKT elements.
* @throws IllegalArgumentException if a {@code codespace:version:code} tuple is assigned twice.
* @throws FactoryDataException if the WKT does not have an {@code ID[…]} or {@code AUTHORITY[…]} element.
*
* @see #definitions
*/
private void addDefinition(final StoredTree tree) throws FactoryDataException {
final Object[] fullId = new Object[authorities == null ? 4 : 3];
tree.peekIdentifiers(fullId); // Codespace, code, version, (authority).
final String code = trimOrNull(fullId[1]);
if (code == null) {
throw new FactoryDataException(resources().getString(Resources.Keys.MissingAuthorityCode_1, tree));
}
final String codespace = trimOrNull(fullId[0]);
definitions.merge(code, tree, (oldValue, newValue) -> {
return new Disambiguation(codespace, trimOrNull(fullId[2]), code, oldValue, newValue);
});
codespaces.add(codespace);
if (fullId.length >= 4) {
final String title = trimOrNull(fullId[3]);
if (title != null) {
authorities.add(title);
}
}
}
/**
* 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 exactly one geodetic object.
* A geodetic object cannot have its definition split 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 cannot 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 {
/*
* 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 StoredTree tree = parser.textToTree(wkt, pos, null);
final int end = pos.getIndex();
if (end < CharSequences.skipTrailingWhitespaces(wkt, 0, wkt.length())) {
throw new FactoryDataException(unexpectedText(lineNumber, wkt, end));
}
addDefinition(tree);
pos.setIndex(0);
lineNumber++;
}
parser.logWarnings(WKTDictionary.class, "addDefinitions");
} catch (ParseException | IllegalArgumentException e) {
throw new FactoryDataException(resources().getString(
Resources.Keys.CanNotParseWKT_2, lineNumber, e.getLocalizedMessage()));
} finally {
parser.clear();
updateAuthority();
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Parses immediately the given WKT and caches the result under the given identifier. This method is invoked
* only if subclass overrides {@link #fetchDefinition(DefaultIdentifier)} for producing WKT on-the-fly.
*
* @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 wkt the Well-Known Text to parse immediately.
* @param defaultIdentifier identifier to assign to the object if the WKT does not provide one.
* @return the parsed object.
* @throws FactoryException if parsing failed.
*/
private IdentifiedObject parseAndAdd(final String codespace, final String version,
final String code, final String wkt, final Identifier defaultIdentifier) throws FactoryException
{
ArgumentChecks.ensureNonEmpty("code", code);
ArgumentChecks.ensureNonEmpty("wkt", wkt);
lock.writeLock().lock();
try {
try {
parser.setDefaultIdentifier(defaultIdentifier);
final Object object = parser.parseObject(wkt);
if (!(object instanceof IdentifiedObject)) {
throw new FactoryDataException(parser.errors().getString(
Errors.Keys.UnexpectedTypeForReference_3, code, IdentifiedObject.class, object.getClass()));
}
final Disambiguation entry = (Disambiguation) definitions.compute(code, (key, oldValue) -> {
return new Disambiguation(codespace, version, code, oldValue, object);
});
codespaces.add(entry.codespace);
return (IdentifiedObject) object;
} catch (ParseException | IllegalArgumentException e) {
throw new FactoryDataException(e.getLocalizedMessage());
} finally {
parser.setDefaultIdentifier(null);
parser.clear();
updateAuthority();
}
} finally {
lock.writeLock().unlock();
}
}
/**
* Fetches the Well-Known Text for a user-specified identifier not found in this {@code WKTDictionary}.
* Subclasses can override this method if WKT strings are not {@linkplain #load(BufferedReader) loaded}
* or {@linkplain #addDefinitions(Stream) specified} in advance, but instead fetched when first needed.
* An example of such scenario is WKTs provided by the {@code "spatial_ref_sys"} table of a spatial database.
* If no WKT is found for the given identifier, then this method returns {@code null}.
*
* <p>On input, {@code identifier} contains only the pieces of information provided by user. For example if user
* invoked {@code createGeographicCRS("Foo")}, then the identifier {@linkplain DefaultIdentifier#getCode() code}
* will be {@code "Foo"} but the {@linkplain DefaultIdentifier#getCodeSpace() codespace} and
* {@linkplain DefaultIdentifier#getVersion() version} will be undefined ({@code null}).
* On output, {@code identifier} should be completed with missing code space and version (if available).</p>
*
* <h4>Overriding</h4>
* The default implementation returns {@code null}. If a subclass overrides this method, then it should
* also override {@link #getAuthorityCodes(Class)} because {@code WKTDictionary} does not know the codes
* that this method can recognize.
*
* @param identifier the code specified by user, possible with code space and version.
* @return Well-Known Text (WKT) for the given identifier, or {@code null} if none.
* @throws FactoryException if an error occurred while fetching the WKT.
*/
protected String fetchDefinition(DefaultIdentifier identifier) throws FactoryException {
return null;
}
/**
* Produces an error message for unexpected characters at the end of WKT string.
*
* @param lineNumber line where the error occurred.
* @param wkt the WKT being parsed.
* @param end end of WKT parsing.
* @return message to give to exception constructor.
*/
private String unexpectedText(final int lineNumber, final String wkt, final int end) {
return resources().getString(Resources.Keys.UnexpectedTextAtLine_2, lineNumber, CharSequences.token(wkt, end));
}
/**
* 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;
}
/**
* Adds all definition values to the given supplier. This is for testing purposes only.
* This method performs no locking because it is not needed for current JUnit tests.
*
* @see StoredTree#forEachValue(Consumer)
*/
final void forEachValue(final Consumer<Object> addTo) {
for (final Object value : definitions.values()) {
if (value instanceof Disambiguation) {
Disambiguation choices = (Disambiguation) value;
do {
addTo.accept(choices.value);
choices = choices.previous;
} while (choices != null);
} else {
addTo.accept(value);
}
}
}
/**
* 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 CollectionsExt.copyPreserveOrder(codespaces);
} finally {
lock.readLock().unlock();
}
}
/**
* 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 {
if (!type.isInterface()) {
type = ReferencingUtilities.getInterface(IdentifiedObject.class, type);
}
Set<String> codes;
lock.readLock().lock();
try {
codes = codeCaches.get(type);
} finally {
lock.readLock().unlock();
}
if (codes == null) {
final String[] keywords = WKTKeywords.forType(type);
final Class<? extends IdentifiedObject> baseType = type; // Because lambdas require final.
final Predicate<Object> filter = (element) -> {
if (element instanceof StoredTree) {
return (keywords == null) || ArraysExt.containsIgnoreCase(keywords, ((StoredTree) element).keyword());
} else {
return baseType.isInstance(element);
}
};
lock.writeLock().lock();
try {
codes = codeCaches.get(type); // In case it has been computed concurrently.
if (codes == null) {
codes = new HashSet<>();
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, filter, codes);
} else if (filter.test(value)) {
codes.add(code);
}
}
/*
* Verify if an existing collection (assigned to another type) provides the same values.
* If we find one, share the same instance for reducing memory usage.
*/
for (final Set<String> other : codeCaches.values()) {
if (codes.equals(other)) {
codes = other;
break;
}
}
codes = Set.copyOf(codes);
codeCaches.put(type, codes);
}
} finally {
lock.writeLock().unlock();
}
}
return codes;
}
/**
* Gets a description of the object corresponding to a code.
*
* @param type the type of object for which to get a description.
* @param code value allocated by authority.
* @return a description of the object, or {@code null} if {@code null} if none.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the query failed for some other reason.
*
* @since 1.5
*/
@Override
public Optional<InternationalString> getDescriptionText(Class<? extends IdentifiedObject> type, final String code)
throws FactoryException
{
final InternationalString name;
final Object value = getOrCreate(code, false);
if (value instanceof IdentifiedObject) {
name = IdentifiedObjects.getDisplayName((IdentifiedObject) value);
} else {
final String text = String.valueOf(value);
if (!(value instanceof StoredTree)) {
// Exception message saved in a previous invocation of `getOrCreate(…)`.
throw new FactoryException(text);
}
name = new SimpleInternationalString(text);
}
return Optional.ofNullable(name);
}
/**
* 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 {
final Object value = getOrCreate(code, true);
if (value instanceof IdentifiedObject) {
return (IdentifiedObject) value;
} else {
// Exception message saved in a previous invocation of `getOrCreate(…)`.
throw new FactoryException(String.valueOf(value));
}
}
/**
* Returns the object associated to the given code.
*
* @param code value allocated by authority.
* @param create whether to create {@link IdentifiedObject} from {@link StoredTree}.
* @return the object for the given code, possibly as a {@link StoredTree} if {@code create} is {@code false}.
* @throws NoSuchAuthorityCodeException if the specified {@code code} was not found.
* @throws FactoryException if the object creation failed for some other reason.
*/
private Object getOrCreate(final String code, final boolean create) 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 `StoredTree` (the `Disambiguation` case
* is resolved as an `IdentifiedObject` or `StoredTree`).
*/
Disambiguation choices = null;
Object value = null;
lock.readLock().lock();
try {
boolean valid = Strings.isNullOrEmpty(codespace) || 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 the value has not been found, check if subclass has a mechanism for fetching WKT
* when first needed. It happens for example if subclass get WKT definitions from the
* "spatial_ref_syst" table of a database.
*/
if (value == null) {
final DefaultIdentifier identifier = new DefaultIdentifier(codespace, localCode, version);
final String wkt = fetchDefinition(identifier);
if (wkt != null) {
return parseAndAdd(codespace, version, localCode, wkt, identifier);
}
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:
*
* - `StoredTree` — if this method is invoked for the first time for the given code.
* - `IdentifiedObject` — if we already built the geodetic object in a previous invocation of this method.
* - `String` — if a previous invocation for given code failed to build the geodetic object.
* In this case, the string is the exception message.
*
* If `StoredTree`, try to replace that value by an `IdentifiedObject` (on success) or `String` (on failure).
* Must be done under write lock because `parser` is not thread-safe.
*/
if (create && value instanceof StoredTree) {
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 StoredTree) {
ParseException cause = null;
try {
value = parser.buildFromTree((StoredTree) value);
parser.logWarnings(WKTDictionary.class, "createObject"); // `createObject` is the public facade.
} 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);
}
codeCaches.clear();
if (cause != null) {
throw new FactoryException(resources().getString(
Resources.Keys.CanNotInstantiateGeodeticObject_1, code), cause);
}
}
} finally {
lock.writeLock().unlock();
}
}
return value;
}
}