| /* |
| * 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; |
| } |
| } |