blob: 01a1e8d00b9ad37f1284f66d694387895407a463 [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.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.Set;
import java.util.Map;
import java.util.HashMap;
import java.util.TreeMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import java.util.function.Function;
import java.net.URI;
import java.io.IOException;
import java.text.Format;
import java.text.NumberFormat;
import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.ParseException;
import javax.measure.Unit;
import org.opengis.util.Factory;
import org.opengis.util.InternationalString;
import org.opengis.metadata.citation.Citation;
import org.opengis.referencing.IdentifiedObject;
import org.opengis.referencing.cs.CSFactory;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.datum.DatumFactory;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.CoordinateOperationFactory;
import org.apache.sis.io.CompoundFormat;
import org.apache.sis.measure.UnitFormat;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.OptionalCandidate;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.system.Loggers;
import org.apache.sis.util.privy.Constants;
import org.apache.sis.util.privy.StandardDateFormat;
import org.apache.sis.referencing.ImmutableIdentifier;
import org.apache.sis.referencing.privy.ReferencingFactoryContainer;
// Specific to the main branch:
import org.apache.sis.metadata.iso.DefaultIdentifier;
/**
* Parser and formatter for <i>Well Known Text</i> (WKT) strings.
* This format handles a pair of {@link org.apache.sis.io.wkt.Parser} and {@link Formatter},
* used by the {@code parse(…)} and {@code format(…)} methods respectively.
* {@code WKTFormat} objects allow the following configuration:
*
* <ul>
* <li>The preferred authority of {@linkplain IdentifiedObject#getName() object name} to
* format (see {@link Formatter#getNameAuthority()} for more information).</li>
* <li>The {@linkplain Symbols symbols} to use (curly braces or brackets, <i>etc</i>).</li>
* <li>The {@linkplain Transliterator transliterator} to use for replacing Unicode characters by ASCII ones.</li>
* <li>Whether ANSI X3.64 colors are allowed or not (default is not).</li>
* <li>The indentation.</li>
* </ul>
*
* <h2>String expansion</h2>
* Because the strings to be parsed by this class are long and tend to contain repetitive substrings,
* {@code WKTFormat} provides a mechanism for performing string substitutions before the parsing take place.
* Long strings can be assigned short names by calls to the {@link #addFragment(String, String)} method.
* After fragments have been added, any call to a parsing method will replace all occurrences (except in
* quoted text) of tokens like {@code $foo} by the WKT fragment named "foo".
*
* <h3>Example</h3>
* In the example below, the {@code $WGS84} substring which appear in the argument given to the
* {@code parseObject(…)} method will be expanded into the full {@code GeodeticCRS[“WGS84”, …]}
* string before the parsing proceed.
*
* <blockquote><code>
* {@linkplain #addFragment addFragment}("deg", "AngleUnit[“degree”, 0.0174532925199433]");<br>
* {@linkplain #addFragment addFragment}("lat", "Axis[“Latitude”, NORTH, <strong>$deg</strong>]");<br>
* {@linkplain #addFragment addFragment}("lon", "Axis[“Longitude”, EAST, <strong>$deg</strong>]");<br>
* {@linkplain #addFragment addFragment}("MyBaseCRS", "GeodeticCRS[“WGS84”, Datum[</code> <i>…etc…</i> <code>],
* CS[</code> <i>…etc…</i> <code>], <strong>$lat</strong>, <strong>$lon</strong>]");<br>
* Object crs = {@linkplain #parseObject(String) parseObject}("ProjectedCRS[“Mercator_1SP”, <strong>$MyBaseCRS</strong>,
* </code> <i>…etc…</i> <code>]");
* </code></blockquote>
*
* Note that the parsing of WKT fragment does not always produce the same object.
* In particular, the default linear and angular units depend on the context in which the WKT fragment appears.
*
* <h2>Limitations</h2>
* <ul>
* <li><strong>The WKT format is not lossless!</strong>
* Objects formatted by {@code WKTFormat} are not guaranteed to be identical after parsing.
* Some metadata may be lost or altered, but the coordinate operations between two CRS should produce
* the same numerical results provided that the two CRS were formatted independently
* (not as inner element of another WKT).</li>
* <li>Instances of this class are not synchronized for multi-threading.
* It is recommended to create separated format instances for each thread.
* If multiple threads access a {@code WKTFormat} concurrently, it must be synchronized externally.</li>
* <li>Serialized objects of this class are not guaranteed to be compatible with future Apache SIS releases.
* Serialization support is appropriate for short term storage or RMI between applications running the
* same version of Apache SIS.</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
* @author Rémi Eve (IRD)
* @version 1.5
*
* @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>
*
* @since 0.4
*/
public class WKTFormat extends CompoundFormat<Object> {
/**
* The logger for Well Known Text operations.
*/
private static final Logger LOGGER = Logger.getLogger(Loggers.WKT);
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -2909110214650709560L;
/**
* The indentation value to give to the {@link #setIndentation(int)}
* method for formatting the complete object on a single line.
*
* @see #getIndentation()
* @see #setIndentation(int)
* @see org.apache.sis.setup.OptionKey#INDENTATION
*/
public static final int SINGLE_LINE = -1;
/**
* The {@linkplain Symbols#immutable() immutable} set of symbols to use for this formatter.
* The same object is also referenced in the {@linkplain #parser} and {@linkplain #formatter}.
* It appears here for serialization purpose.
*
* @see #setSymbols(Symbols)
*/
private Symbols symbols;
/**
* The {@linkplain Colors#immutable() immutable} set of colors to use for this formatter,
* or {@code null} for no syntax coloring. The default value is {@code null}.
* The same object is also referenced in the {@linkplain #formatter}.
* It appears here for serialization purpose.
*
* @see #setColors(Colors)
*/
private Colors colors;
/**
* The convention to use. The same object is also referenced in the {@linkplain #formatter}.
* It appears here for serialization purpose.
*/
private Convention convention;
/**
* The preferred authority for objects or parameter names. A {@code null} value
* means that the authority shall be inferred from the {@linkplain #convention}.
*/
@SuppressWarnings("serial") // Most SIS implementations are serializable.
private Citation authority;
/**
* Whether WKT keywords shall be formatted in upper case.
*/
private KeywordCase keywordCase;
/**
* Whether to use short or long WKT keywords.
*/
private KeywordStyle keywordStyle;
/**
* {@link Transliterator#IDENTITY} for preserving non-ASCII characters. The default value is
* {@link Transliterator#DEFAULT}, which causes replacements like "é" → "e" in all elements
* except {@code REMARKS["…"]}. May also be a user supplied transliterator.
*
* <p>A {@code null} value means to infer this property from the {@linkplain #convention}.</p>
*/
private Transliterator transliterator;
/**
* The number of spaces to use in indentation, or {@value #SINGLE_LINE} if indentation is disabled.
* The same value is also stored in the {@linkplain #formatter}.
* It appears here for serialization purpose.
*/
private byte indentation;
/**
* Maximum number of elements to show in lists, or {@link Integer#MAX_VALUE} if unlimited.
* If a list is longer than this length, only the first and the last elements will be shown.
* This limit applies in particular to {@link org.opengis.referencing.operation.MathTransform}
* parameter values of {@code double[]} type, since those parameters may be large interpolation tables.
*
* @see #getMaximumListElements()
*/
private int listSizeLimit;
/**
* Identifier to assign to parsed {@link IdentifiedObject} if the WKT does not contain an
* explicit {@code ID[…]} or {@code AUTHORITY[…]} element. The main use case is for implementing
* a {@link org.opengis.referencing.crs.CRSAuthorityFactory} backed by definitions in WKT format.
*
* <p>This field is transient because this is not yet a public API. The {@code transient}
* keyword may be removed in a future version if we commit to this API.</p>
*
* @see #setDefaultIdentifier(DefaultIdentifier)
*/
private transient DefaultIdentifier defaultIdentifier;
/**
* WKT fragments that can be inserted in longer WKT strings, or {@code null} if none. Keys are short identifiers
* and values are WKT subtrees to substitute to the identifiers when they are found in a WKT to parse.
* The same map instance may be shared by different {@linkplain #clone() clones} as long as they are not modified.
*
* @see #fragments(boolean)
*/
private TreeMap<String,StoredTree> fragments;
/**
* The URI to declare as the source of the WKT definitions, or {@code null} if unknown.
* This information is not used directly by the parser, but will be stored in parameter values
* as a hint for resolving relative paths as absolute paths.
*
* @see #getSourceFile()
*/
private URI sourceFile;
/**
* {@code true} if the {@link #fragments} map is shared by two or more {@code WKTFormat} instances.
* In such case, the map shall not be modified; instead it must be copied before any modification.
*
* <h4>Use case</h4>
* This flag allows to clone the {@link #fragments} map only when first needed. In use cases where
* {@code WKTFormat} is cloned for multi-threading purposes without change in its configuration,
* this flag avoids completely the need to clone the {@link #fragments} map.
*
* @see #clone()
* @see #fragments(boolean)
*/
private transient boolean isCloned;
/**
* Temporary map used by {@link #addFragment(String, String)} for reusing existing instances when possible.
* Keys and values are the same {@link String}, {@link Boolean}, {@link Number} or {@link Date} instances.
*
* <p>This reference is set to null when we assume that no more fragments will be added to this format.
* It is not a problem if this map is destroyed too aggressively, since it will be recreated when needed.
* The only cost of destroying the map too aggressively is that we may have more instance duplications
* than what we would otherwise have.</p>
*/
private transient Map<Object,Object> sharedValues;
/**
* A formatter using the same symbols as the {@linkplain #parser}.
* Will be created by the {@link #format(Object, Appendable)} method when first needed.
*/
private transient Formatter formatter;
/**
* The parser. Will be created when first needed.
*/
private transient AbstractParser parser;
/**
* The factories needed by the parser. Those factories are currently not serialized (because usually not
* serializable), so any value that users may have specified with {@link #setFactory(Class, Factory)}
* will be lost at serialization time.
*
* @see #factories()
*/
private transient ReferencingFactoryContainer factories;
/**
* The warning produced by the last parsing or formatting operation, or {@code null} if none.
*
* @see #getWarnings()
*/
private transient Warnings warnings;
/**
* Creates a format for the root locale and UTC timezone.
* This is the standard configuration for ISO 19162 Well-Known Text.
*
* @since 1.5
*/
public WKTFormat() {
this(null, null);
}
/**
* Creates a format for the given locale and timezone. The given locale will be used for
* {@link InternationalString} localization, <strong>not</strong> for formatting numbers.
* The given timezone will be used for parsing and formatting dates in temporal elements
* such as {@code TimeOrigin[…]}. Note that the specified timezone will not be formatted
* in WKT elements, as it will be assumed implicit.
*
* @param locale the locale for the new {@code Format}, or {@code null} for {@code Locale.ROOT}.
* @param timezone the timezone for dates in the WKT temporal elements, or {@code null} for UTC.
*
* @see #getLocale()
* @see #getTimeZone()
*/
public WKTFormat(final Locale locale, final TimeZone timezone) {
super(locale, timezone);
convention = Convention.DEFAULT;
symbols = Symbols.getDefault();
keywordCase = KeywordCase.DEFAULT;
keywordStyle = KeywordStyle.DEFAULT;
indentation = Constants.DEFAULT_INDENTATION;
listSizeLimit = Integer.MAX_VALUE;
}
/**
* Returns the {@link #fragments} map, creating it when first needed.
* Caller shall not modify the returned map, unless the {@code modifiable} parameter is {@code true}.
*
* @param modifiable whether the caller intents to modify the map.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
private Map<String,StoredTree> fragments(final boolean modifiable) {
if (fragments == null) {
if (!modifiable) {
// Most common cases: invoked before to parse a WKT and no fragments specified.
return Map.of();
}
fragments = new TreeMap<>();
isCloned = false;
} else if (isCloned & modifiable) {
fragments = new TreeMap<>(fragments);
isCloned = false;
}
return fragments;
}
/**
* Returns the container of {@link #factories}, creating it when first needed.
* This container is needed at parsing time but not at formatting time.
*/
@SuppressWarnings("ReturnOfCollectionOrArrayField")
private ReferencingFactoryContainer factories() {
if (factories == null) {
factories = new ReferencingFactoryContainer();
}
return factories;
}
/**
* Returns the locale for the given category. This method implements the following mapping:
*
* <ul>
* <li>{@link java.util.Locale.Category#FORMAT}: the value of {@link Symbols#getLocale()},
* normally fixed to {@link Locale#ROOT}, used for number formatting.</li>
* <li>{@link java.util.Locale.Category#DISPLAY}: the {@code locale} given at construction time,
* used for {@link InternationalString} localization.</li>
* </ul>
*
* @param category the category for which a locale is desired.
* @return the locale for the given category (never {@code null}).
*/
@Override
public Locale getLocale(final Locale.Category category) {
if (category == Locale.Category.FORMAT) {
return symbols.getLocale();
}
return super.getLocale(category);
}
/**
* Returns the locale to use for error messages. Other {@link CompoundFormat} classes use the system default.
* But this class uses a compromise: not exactly the locale used for {@link InternationalString} because that
* locale is often fixed to English, and not exactly the system default neither because this "error locale"
* is also used for warnings. The compromise implemented in this method may change in any future version.
*
* @see #errors()
*/
final Locale getErrorLocale() {
final Locale locale = getLocale(Locale.Category.DISPLAY);
return (locale != null && locale != Locale.ROOT) ? locale : Locale.getDefault(Locale.Category.DISPLAY);
}
/**
* Returns the symbols used for parsing and formatting WKT. This method returns an unmodifiable instance.
* Modifications, if desired, should be applied on a {@linkplain Symbols#clone() clone} of the returned object.
*
* @return the current set of symbols used for parsing and formatting WKT.
*/
public Symbols getSymbols() {
return symbols;
}
/**
* Sets the symbols used for parsing and formatting WKT.
*
* @param style the new set of symbols to use for parsing and formatting WKT.
*/
public void setSymbols(final Symbols style) {
if (!style.equals(symbols)) { // Intentional NullPointerException.
symbols = style.immutable();
formatter = null;
parser = null;
}
}
/**
* Returns a mapper between Java character sequences and the characters to write in WKT.
* The intent is to specify how to write characters that are not allowed in WKT strings
* according ISO 19162 specification. Return values can be:
*
* <ul>
* <li>{@link Transliterator#DEFAULT} for performing replacements like "é" → "e"
* in all WKT elements except {@code REMARKS["…"]}.</li>
* <li>{@link Transliterator#IDENTITY} for preserving non-ASCII characters.</li>
* <li>Any other user supplied mapping.</li>
* </ul>
*
* @return the mapper between Java character sequences and the characters to write in WKT.
*
* @since 0.6
*/
public Transliterator getTransliterator() {
Transliterator result = transliterator;
if (result == null) {
result = (convention == Convention.INTERNAL) ? Transliterator.IDENTITY : Transliterator.DEFAULT;
}
return result;
}
/**
* Sets the mapper between Java character sequences and the characters to write in WKT.
*
* <p>If this method is never invoked, or if this method is invoked with a {@code null} value,
* then the default mapper is {@link Transliterator#DEFAULT} except for WKT formatted according
* the {@linkplain Convention#INTERNAL internal convention}.</p>
*
* @param map the new mapper to use, or {@code null} for restoring the default value.
*
* @since 0.6
*/
public void setTransliterator(final Transliterator map) {
if (transliterator != map) {
transliterator = map;
updateFormatter(formatter);
parser = null;
}
}
/**
* Returns whether WKT keywords should be written with upper cases or camel cases.
*
* @return the case to use for formatting keywords.
*/
public KeywordCase getKeywordCase() {
return keywordCase;
}
/**
* Sets whether WKT keywords should be written with upper cases or camel cases.
*
* @param style the case to use for formatting keywords.
*/
public void setKeywordCase(final KeywordCase style) {
keywordCase = Objects.requireNonNull(style);
updateFormatter(formatter);
}
/**
* Returns whether to use short or long WKT keywords.
*
* @return the style used for formatting keywords.
*
* @since 0.6
*/
public KeywordStyle getKeywordStyle() {
return keywordStyle;
}
/**
* Sets whether to use short or long WKT keywords.
*
* @param style the style to use for formatting keywords.
*
* @since 0.6
*/
public void setKeywordStyle(final KeywordStyle style) {
keywordStyle = Objects.requireNonNull(style);
updateFormatter(formatter);
}
/**
* Returns the colors to use for syntax coloring, or {@code null} if none.
* This method returns an unmodifiable instance. Modifications, if desired,
* should be applied on a {@linkplain Colors#clone() clone} of the returned object.
* By default there is no syntax coloring.
*
* @return the colors for syntax coloring, or {@code null} if none.
*/
public Colors getColors() {
return colors;
}
/**
* Sets the colors to use for syntax coloring.
* This property applies only when formatting text.
*
* <p>Newly created {@code WKTFormat}s have no syntax coloring. If a non-null argument like
* {@link Colors#DEFAULT} is given to this method, then the {@link #format(Object, Appendable) format(…)}
* method tries to highlight most of the elements that are relevant to
* {@link org.apache.sis.util.Utilities#equalsIgnoreMetadata(Object, Object)}.</p>
*
* @param emphasis the colors for syntax coloring, or {@code null} if none.
*/
public void setColors(Colors emphasis) {
if (emphasis != null) {
emphasis = emphasis.immutable();
}
colors = emphasis;
updateFormatter(formatter);
}
/**
* Returns the convention for parsing and formatting WKT elements.
* The default value is {@link Convention#WKT2}.
*
* @return the convention to use for formatting WKT elements (never {@code null}).
*/
public Convention getConvention() {
return convention;
}
/**
* Sets the convention for parsing and formatting WKT elements.
*
* @param variant the new convention to use for parsing and formatting WKT elements.
*/
public void setConvention(final Convention variant) {
if (convention != variant) {
convention = Objects.requireNonNull(variant);
updateFormatter(formatter);
parser = null;
}
}
/**
* Returns the preferred authority to look for when fetching identified object names and identifiers.
* The difference between various authorities are most easily seen in projection and parameter names.
* If no authority has been {@linkplain #setNameAuthority(Citation) explicitly set}, then this
* method returns the default authority for the current {@linkplain #getConvention() convention}.
*
* <h4>Example</h4>
* The following table shows the names given by various organizations or projects for the same projection:
*
* <table class="sis">
* <caption>Projection name examples</caption>
* <tr><th>Authority</th> <th>Projection name</th></tr>
* <tr><td>EPSG</td> <td>Mercator (variant A)</td></tr>
* <tr><td>OGC</td> <td>Mercator_1SP</td></tr>
* <tr><td>GEOTIFF</td> <td>CT_Mercator</td></tr>
* </table>
*
* @return the organization, standard or project to look for when fetching projection and parameter names.
*
* @see Formatter#getNameAuthority()
*/
public Citation getNameAuthority() {
Citation result = authority;
if (result == null) {
result = convention.getNameAuthority();
}
return result;
}
/**
* Sets the preferred authority for choosing the projection and parameter names.
* If non-null, the given priority will have precedence over the authority usually
* associated to the {@linkplain #getConvention() convention}. A {@code null} value
* restore the default behavior.
*
* @param authority the new authority, or {@code null} for inferring it from the convention.
*
* @see Formatter#getNameAuthority()
*/
public void setNameAuthority(final Citation authority) {
this.authority = authority;
updateFormatter(formatter);
// No need to update the parser.
}
/**
* Updates the formatter convention, authority, colors and indentation according the current state of this
* {@code WKTFormat}. The authority may be null, in which case it will be inferred from the convention when
* first needed.
*/
private void updateFormatter(final Formatter formatter) {
if (formatter != null) {
final byte toUpperCase;
switch (keywordCase) {
case LOWER_CASE: toUpperCase = -1; break;
case UPPER_CASE: toUpperCase = +1; break;
case CAMEL_CASE: toUpperCase = 0; break;
default: toUpperCase = convention.toUpperCase ? (byte) +1 : 0; break;
}
final byte longKeywords;
switch (keywordStyle) {
case SHORT: longKeywords = -1; break;
case LONG: longKeywords = +1; break;
default: longKeywords = (convention.majorVersion() == 1) ? (byte) -1 : 0; break;
}
formatter.configure(convention, authority, colors, toUpperCase, longKeywords, indentation, listSizeLimit);
if (transliterator != null) {
formatter.transliterator = transliterator;
}
}
}
/**
* Returns the current indentation to be used for formatting objects.
* The {@value #SINGLE_LINE} value means that the whole WKT is to be formatted on a single line.
*
* @return the current indentation.
*/
public int getIndentation() {
return indentation;
}
/**
* Sets a new indentation to be used for formatting objects.
* The {@value #SINGLE_LINE} value means that the whole WKT is to be formatted on a single line.
*
* @param numSpaces the new indentation to use in number of spaces.
*
* @see org.apache.sis.setup.OptionKey#INDENTATION
*/
public void setIndentation(final int numSpaces) {
ArgumentChecks.ensureBetween("indentation", SINGLE_LINE, Byte.MAX_VALUE, numSpaces);
indentation = (byte) numSpaces;
updateFormatter(formatter);
}
/**
* Returns the maximum number of elements to show in lists of values. If a list length is greater than this limit,
* then only the first and last elements will be shown together with a message saying that some elements were omitted.
* This limit is useful in particular with {@link org.opengis.referencing.operation.MathTransform} parameter values of
* {@code double[]} type, since those parameters may be large interpolation tables.
*
* @return the current lists size limit, or {@link Integer#MAX_VALUE} if unlimited.
*
* @since 1.0
*/
public int getMaximumListElements() {
return listSizeLimit;
}
/**
* Sets a new limit for the number of elements to show in lists.
* If this method is never invoked, then the default is unlimited.
*
* @param limit the new lists size limit, or {@link Integer#MAX_VALUE} if unlimited.
*
* @since 1.0
*/
public void setMaximumListElements(final int limit) {
ArgumentChecks.ensureStrictlyPositive("listSizeLimit", limit);
listSizeLimit = limit;
updateFormatter(formatter);
}
/**
* Sets the identifier to assign to parsed {@link IdentifiedObject} if the WKT does not contain an
* explicit {@code ID[…]} or {@code AUTHORITY[…]} element. The main use case is for implementing
* a {@link org.opengis.referencing.crs.CRSAuthorityFactory} backed by definitions in WKT format.
*
* <p>Note that this identifier applies to all objects to be created, which is generally not desirable.
* Callers should invoke {@code setDefaultIdentifier(null)} in a {@code finally} block.</p>
*
* <p>This is not a publicly committed API. If we want to make this functionality public in a future
* version, we should investigate if we should make it applicable to a wider range of properties and
* how to handle the fact that the a given identifier should be used for only one object.</p>
*
* @param identifier the default identifier, or {@code null} if none.
*/
final void setDefaultIdentifier(final DefaultIdentifier identifier) {
defaultIdentifier = identifier;
}
/**
* Verifies if the given type is a valid key for the {@link #factories} map.
*/
private void ensureValidFactoryType(final Class<?> type) throws IllegalArgumentException {
if (type != CRSFactory.class &&
type != CSFactory.class &&
type != DatumFactory.class &&
type != MathTransformFactory.class &&
type != CoordinateOperationFactory.class)
{
ArgumentChecks.ensureNonNull("type", type);
throw new IllegalArgumentException(errors().getString(Errors.Keys.IllegalArgumentValue_2, "type", type));
}
}
/**
* Returns one of the factories used by this {@code WKTFormat} for parsing WKT.
* The given {@code type} argument can be one of the following values:
*
* <ul>
* <li><code>{@linkplain CRSFactory}.class</code></li>
* <li><code>{@linkplain CSFactory}.class</code></li>
* <li><code>{@linkplain DatumFactory}.class</code></li>
* <li><code>{@linkplain MathTransformFactory}.class</code></li>
* <li><code>{@linkplain CoordinateOperationFactory}.class</code></li>
* </ul>
*
* @param <T> the compile-time type of the {@code type} argument.
* @param type the factory type.
* @return the factory used by this {@code WKTFormat} for the given type.
* @throws IllegalArgumentException if the {@code type} argument is not one of the valid values.
*/
public <T extends Factory> T getFactory(final Class<T> type) {
ensureValidFactoryType(type);
return factories().getFactory(type);
}
/**
* Sets one of the factories to be used by this {@code WKTFormat} for parsing WKT.
* The given {@code type} argument can be one of the following values:
*
* <ul>
* <li><code>{@linkplain CRSFactory}.class</code></li>
* <li><code>{@linkplain CSFactory}.class</code></li>
* <li><code>{@linkplain DatumFactory}.class</code></li>
* <li><code>{@linkplain MathTransformFactory}.class</code></li>
* <li><code>{@linkplain CoordinateOperationFactory}.class</code></li>
* </ul>
*
* <h4>Limitation</h4>
* The current implementation does not serialize the given factories, because they are usually not
* {@link java.io.Serializable}. The factories used by {@code WKTFormat} instances after deserialization
* are the default ones.
*
* @param <T> the compile-time type of the {@code type} argument.
* @param type the factory type.
* @param factory the factory to be used by this {@code WKTFormat} for the given type.
* @throws IllegalArgumentException if the {@code type} argument is not one of the valid values.
*/
public <T extends Factory> void setFactory(final Class<T> type, final T factory) {
ensureValidFactoryType(type);
if (factories().setFactory(type, factory)) {
parser = null;
}
}
/**
* Returns the URI to declare as the source of the WKT definitions. This value is not used directly by the parser,
* but stored as a hint for allowing users to interpret the {@code PARAMETERFILE[…]} value as a file relative to
* the file containing the WKT. The default value is {@code null}.
*
* @return the URI to declare as the source of the WKT definitions, or {@code null} if none.
*
* @see #setSourceFile(URI)
* @see org.apache.sis.parameter.DefaultParameterValue#getSourceFile()
* @see org.apache.sis.xml.MarshalContext#getDocumentURI()
* @see URI#resolve(URI)
*
* @since 1.5
*/
public URI getSourceFile() {
return sourceFile;
}
/**
* Sets the URI to declare as the source of the WKT definitions. This information will be stored in
* {@link org.apache.sis.parameter.DefaultParameterValue#getSourceFile()} at WKT parsing time as a
* hint for resolving relative paths as absolute paths. This value has no effect at formatting time.
*
* @param document URI to the file that contains the WKT definitions to parse, or {@code null} if none.
*
* @see #getSourceFile()
* @see org.apache.sis.parameter.DefaultParameterValue#setSourceFile(URI)
* @see URI#resolve(URI)
*
* @since 1.5
*/
public void setSourceFile(final URI document) {
if (!Objects.equals(sourceFile, document)) {
sourceFile = document;
parser = null;
}
}
/**
* Returns the type of objects formatted by this class. This method has to return {@code Object.class}
* because it is the only common parent to all object types accepted by this formatter.
*
* @return {@code Object.class}
*/
@Override
public final Class<Object> getValueType() {
return Object.class;
}
/**
* Returns the name of all WKT fragments known to this {@code WKTFormat}.
* The returned collection is initially empty.
* WKT fragments can be added by call to {@link #addFragment(String, String)}.
*
* <p>The returned collection is modifiable. In particular, a call to {@link Set#clear()}
* removes all fragments from this {@code WKTFormat}.</p>
*
* @return the name of all fragments known to this {@code WKTFormat}.
*/
public Set<String> getFragmentNames() {
return fragments(true).keySet();
}
/**
* Adds a fragment of Well Know Text (WKT). The {@code wkt} argument given to this method
* can contains itself other fragments specified in some previous calls to this method.
*
* <h4>Example</h4>
* If the following method is invoked:
*
* {@snippet lang="java" :
* addFragment("MyEllipsoid", "Ellipsoid[“Bessel 1841”, 6377397.155, 299.1528128, ID[“EPSG”,“7004”]]");
* }
*
* Then other WKT strings parsed by this {@code WKTFormat} instance can refer to the above fragment as below
* (WKT after the ellipsoid omitted for brevity):
*
* {@snippet lang="java" :
* Object crs = parseObject("GeodeticCRS[“Tokyo”, Datum[“Tokyo”, $MyEllipsoid], …]");
* }
*
* For removing a fragment, use <code>{@linkplain #getFragmentNames()}.remove(name)</code>.
*
* @param name the name to assign to the WKT fragment (case-sensitive). Must be a valid Unicode identifier.
* @param wkt the Well Know Text (WKT) fragment represented by the given identifier.
* @throws IllegalArgumentException if the given name is not a valid Unicode identifier
* or if a fragment is already associated to that name.
* @throws ParseException if an error occurred while parsing the given WKT.
*/
public void addFragment(final String name, final String wkt) throws IllegalArgumentException, ParseException {
ArgumentChecks.ensureNonEmpty("wkt", wkt);
ArgumentChecks.ensureNonEmpty("name", name);
if (!CharSequences.isUnicodeIdentifier(name)) {
throw new IllegalArgumentException(errors().getString(Errors.Keys.NotAUnicodeIdentifier_1, name));
}
final ParsePosition pos = new ParsePosition(0);
final StoredTree definition = textToTree(wkt, pos, name);
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 + " = " + definition.keyword() + "[…]", CharSequences.token(wkt, index)}, index);
}
addFragment(name, definition);
logWarnings(WKTFormat.class, "addFragment");
}
/**
* 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 definition root of the WKT fragment to add.
* @throws IllegalArgumentException if a fragment is already associated to the given name.
*/
final void addFragment(final String name, final StoredTree definition) {
if (fragments(true).putIfAbsent(name, definition) != null) {
throw new IllegalArgumentException(errors().getString(Errors.Keys.ElementAlreadyPresent_1, name));
}
}
/**
* Parses a Well Know Text (WKT) for a fragment or an entire object definition.
* This method should be invoked only for WKT trees to be stored for a long time.
* It should not be invoked for immediate {@link IdentifiedObject} parsing.
*
* <p>If {@code aliasKey} is non-null, this method may return a multi-roots tree.
* See {@link StoredTree#root} for a discussion. Note that in both cases (single
* root or multi-roots), we may have some unparsed characters at the end of the string.</p>
*
* @param wkt the Well Know Text (WKT) fragment to parse.
* @param pos index of the first character to parse (on input) or after last parsed character (on output).
* @param aliasKey key of the alias, or {@code null} if this method is not invoked
* for defining a {@linkplain #addFragment(String, String) fragment}.
* @return root of the tree of elements.
*/
final StoredTree textToTree(final String wkt, final ParsePosition pos, final String aliasKey) throws ParseException {
@SuppressWarnings("LocalVariableHidesMemberVariable")
final AbstractParser parser = parser(true);
final List<Element> results = new ArrayList<>(4);
warnings = null; // Do not invoke `clear()` because we do not want to clear `sharedValues` map.
try {
for (;;) {
results.add(parser.textToTree(wkt, pos));
if (aliasKey == null) break;
/*
* If we find a separator (usually a coma), search for another element. Contrarily to equivalent
* loop in `Element(AbstractParser, …)` constructor, we do not parse number or dates because we
* do not have a way as reliable as above-cited constructor to differentiate the kind of value.
*/
final int p = CharSequences.skipLeadingWhitespaces(wkt, pos.getIndex(), wkt.length());
final String separator = parser.symbols.trimmedSeparator();
if (!wkt.startsWith(separator, p)) break;
pos.setIndex(p + separator.length());
}
} finally {
// Invoked as a matter of principle, but no warning is expected at this stage.
warnings = parser.getAndClearWarnings(results.isEmpty() ? null : results.get(0));
}
if (sharedValues == null) {
sharedValues = new HashMap<>();
}
if (results.size() == 1) {
return new StoredTree(results.get(0), sharedValues); // Standard case.
} else {
return new StoredTree(results, sharedValues); // Anonymous wrapper around multi-roots.
}
}
/**
* Clears warnings and cache of shared values.
*/
final void clear() {
warnings = null;
sharedValues = null;
}
/**
* Creates an object from the given character sequence.
* The parsing begins at the index given by the {@code pos} argument.
* After successful parsing, {@link ParsePosition#getIndex()} gives the position after the last parsed character.
* In case of error, {@link ParseException#getErrorOffset()} gives the position of the first illegal character.
*
* @param wkt the character sequence for the object to parse.
* @param pos index of the first character to parse (on input) or after last parsed character (on output).
* @return the parsed object (never {@code null}).
* @throws ParseException if an error occurred while parsing the WKT.
*/
@Override
public Object parse(final CharSequence wkt, final ParsePosition pos) throws ParseException {
clear();
ArgumentChecks.ensureNonEmpty("wkt", wkt);
ArgumentChecks.ensureNonNull ("pos", pos);
@SuppressWarnings("LocalVariableHidesMemberVariable")
final AbstractParser parser = parser(false);
Object result = null;
try {
result = parser.createFromWKT(wkt.toString(), pos);
} finally {
warnings = parser.getAndClearWarnings(result);
}
return result;
}
/**
* Parses a tree of {@link Element}s to produce a geodetic object. The {@code tree} argument
* should be a value returned by {@link #textToTree(String, ParsePosition, String)}.
* This method is for {@link WKTDictionary#createObject(String)} usage.
*
* @param tree the tree of WKT elements.
* @return the parsed object (never {@code null}).
* @throws ParseException if the tree cannot be parsed.
*/
final Object buildFromTree(StoredTree tree) throws ParseException {
clear();
@SuppressWarnings("LocalVariableHidesMemberVariable")
final AbstractParser parser = parser(false);
parser.ignoredElements.clear();
final SingletonElement singleton = new SingletonElement();
tree.toElements(parser, singleton, 0);
final Element root = new Element(singleton.value);
Object result = null;
try {
result = parser.buildFromTree(root);
root.close(parser.ignoredElements);
} finally {
warnings = parser.getAndClearWarnings(result);
}
return result;
}
/**
* Returns the parser, created when first needed.
*
* @param modifiable whether the caller intents to modify the {@link #fragments} map.
*/
private AbstractParser parser(final boolean modifiable) {
/*
* `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)) {
parser = new Parser(sourceFile, fragments(modifiable), symbols,
(NumberFormat) getFormat(Number.class),
(DateFormat) getFormat(Date.class),
(UnitFormat) getFormat(Unit.class),
convention,
(transliterator != null) ? transliterator : Transliterator.DEFAULT,
getErrorLocale(),
factories());
}
return parser;
}
/**
* The parser created by {@link #parser(boolean)}, identical to {@link GeodeticObjectParser} except
* for the source of logging messages which is the enclosing {@code WKTParser} instead of a factory.
* Also provides a mechanism for adding default identifier to root {@link IdentifiedObject}.
*/
private final class Parser extends GeodeticObjectParser implements Function<Object,Object> {
/** Creates a new parser. */
Parser(final URI sourceFile, final Map<String,StoredTree> fragments, final Symbols symbols,
final NumberFormat numberFormat, final DateFormat dateFormat, final UnitFormat unitFormat,
final Convention convention, final Transliterator transliterator, final Locale errorLocale,
final ReferencingFactoryContainer factories)
{
super(sourceFile, fragments, symbols, numberFormat, dateFormat, unitFormat, convention, transliterator, errorLocale, factories);
}
/** Returns the source class and method to declare in log records. */
@Override String getPublicFacade() {return WKTFormat.class.getName();}
@Override String getFacadeMethod() {return "parse";}
/** Invoked when an identifier need to be supplied to root {@link IdentifiedObject}. */
@Override public Object apply(Object key) {
return new ImmutableIdentifier(defaultIdentifier.getAuthority(), defaultIdentifier.getCodeSpace(),
defaultIdentifier.getCode(), defaultIdentifier.getVersion(), defaultIdentifier.getDescription());
}
/** Invoked when a root {@link IdentifiedObject} is about to be created. */
@Override void completeRoot(final Map<String,Object> properties) {
if (defaultIdentifier != null) {
properties.computeIfAbsent(IdentifiedObject.IDENTIFIERS_KEY, this);
}
}
}
/**
* Formats the specified object as a Well Know Text. The formatter accepts at least the following types:
* {@link FormattableObject}, {@link IdentifiedObject},
* {@link org.opengis.referencing.operation.MathTransform},
* {@link org.opengis.metadata.extent.GeographicBoundingBox},
* {@link org.opengis.metadata.extent.VerticalExtent},
* {@link org.opengis.metadata.extent.TemporalExtent},
* {@link org.opengis.geometry.Envelope},
* {@link org.opengis.geometry.DirectPosition}
* and {@link Unit}.
*
* @param object the object to format.
* @param toAppendTo where the text is to be appended.
* @throws IOException if an error occurred while writing to {@code toAppendTo}.
*
* @see FormattableObject#toWKT()
*/
@Override
public void format(final Object object, final Appendable toAppendTo) throws IOException {
clear();
ArgumentChecks.ensureNonNull("object", object);
ArgumentChecks.ensureNonNull("toAppendTo", toAppendTo);
/*
* If the given Appendable is not a StringBuffer, creates a temporary StringBuffer.
* We cannot write directly in an arbitrary Appendable because Formatter needs the
* ability to go backward ("append only" is not sufficient), and because it passes
* the buffer to other java.text.Format instances which work only with StringBuffer.
*/
final StringBuffer buffer;
if (toAppendTo instanceof StringBuffer) {
buffer = (StringBuffer) toAppendTo;
} else {
buffer = new StringBuffer(500);
}
/*
* Creates the Formatter when first needed.
*/
@SuppressWarnings("LocalVariableHidesMemberVariable")
Formatter formatter = this.formatter;
if (formatter == null) {
formatter = new Formatter(getLocale(), getErrorLocale(), symbols,
(NumberFormat) getFormat(Number.class),
(DateFormat) getFormat(Date.class),
(UnitFormat) getFormat(Unit.class));
updateFormatter(formatter);
this.formatter = formatter;
}
/*
* Since each operation on `buffer` is synchronized, this synchronization block allow the lock
* to be obtained only one time instead of many times for each `StringBuffer` method invoked.
* As a bonus, it ensures a consistent result if the given `StringBuffer` is used concurrently.
*/
synchronized (buffer) {
final boolean valid;
try {
formatter.setBuffer(buffer);
valid = formatter.appendElement(object) || formatter.appendValue(object);
} finally {
warnings = formatter.getWarnings(); // Must be saved before formatter.clear() is invoked.
formatter.setBuffer(null);
formatter.clear();
}
if (warnings != null) {
warnings.setRoot(object);
}
if (!valid) {
throw new ClassCastException(errors().getString(
Errors.Keys.IllegalArgumentClass_2, "object", object.getClass()));
}
if (buffer != toAppendTo) {
toAppendTo.append(buffer);
}
}
}
/**
* Creates a new format to use for parsing and formatting values of the given type.
* This method is invoked the first time that a format is needed for the given type.
* The {@code valueType} can be any types declared in the
* {@linkplain CompoundFormat#createFormat(Class) parent class}.
*
* @param valueType the base type of values to parse or format.
* @return the format to use for parsing of formatting values of the given type, or {@code null} if none.
*/
@Override
protected Format createFormat(final Class<?> valueType) {
if (valueType == Number.class) {
return symbols.createNumberFormat();
}
if (valueType == Date.class) {
return new StandardDateFormat(symbols.getLocale(), getTimeZone());
}
final Format format = super.createFormat(valueType);
if (format instanceof UnitFormat) {
((UnitFormat) format).setStyle(UnitFormat.Style.NAME);
}
return format;
}
/**
* If warnings occurred during the last WKT {@linkplain #parse(CharSequence, ParsePosition) parsing} or
* {@linkplain #format(Object, Appendable) formatting}, returns the warnings. Otherwise returns {@code null}.
* The warnings are cleared every time a new object is parsed or formatted.
*
* @return the warnings of the last parsing of formatting operation, or {@code null} if none.
*
* @since 0.6
*/
@OptionalCandidate
public Warnings getWarnings() {
if (warnings != null) {
warnings.publish();
}
return warnings;
}
/**
* If a warning occurred, logs it. This method is invoked when a WKT was parsed or formatted
* in another context than a call to a {@code parse(…)} or {@code format(…)} method.
* For example it may be during the build of {@link WKTDictionary}.
*
* @param classe the class to report as the source of the logging message.
* @param method the method to report as the source of the logging message.
*/
final void logWarnings(final Class<?> classe, final String method) {
if (warnings != null) {
/*
* We can avoid the call to `Warnings.publish()` because we know that we are not keeping a
* reference for long, so we do not need to copy the `AbstractParser.ignoredElements` map.
*/
final LogRecord record = new LogRecord(Level.WARNING, warnings.toString());
Logging.completeAndLog(LOGGER, classe, method, record);
}
}
/**
* Convenience methods for resources for error message in the locale given by {@link #getLocale()}.
*/
final Errors errors() {
return Errors.forLocale(getErrorLocale());
}
/**
* Returns a clone of this format. The clone has the same configuration (including any added
* {@linkplain #addFragment fragments}), except the {@linkplain #getWarnings() warnings}.
*
* @return a clone of this format.
*/
@Override
public WKTFormat clone() {
final WKTFormat clone = (WKTFormat) super.clone();
clone.clear();
clone.factories = null; // Not thread-safe; clone needs its own.
clone.formatter = null; // Do not share the formatter.
clone.parser = null;
clone.isCloned = isCloned = true;
// Symbols and Colors do not need to be cloned because they are flagged as immutable.
return clone;
}
}