blob: 4ca8c07b40701426d26e63dc01030b69a4db8fc2 [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.Arrays;
import java.util.Locale;
import java.io.Serializable;
import java.io.ObjectStreamException;
import java.text.NumberFormat;
import org.apache.sis.util.Localized;
import org.apache.sis.util.Workaround;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.*;
/**
* The set of symbols to use for <cite>Well Known Text</cite> (WKT) parsing and formatting.
* The two constants defined in this class, namely {@link #SQUARE_BRACKETS} and {@link #CURLY_BRACKETS},
* define the symbols for ISO 19162 compliant WKT formatting. Their properties are:
*
* <table class="sis">
* <caption>Standard WKT symbols</caption>
* <tr>
* <th>WKT aspect</th>
* <th>Standard value</th>
* <th>Comment</th>
* </tr>
* <tr>
* <td>Locale for number format:</td>
* <td>{@link Locale#ROOT}</td>
* <td></td>
* </tr>
* <tr>
* <td>Bracket symbols:</td>
* <td>{@code [}…{@code ]} or {@code (}…{@code )}</td>
* <td><span style="font-size: small"><b>Note:</b> the {@code […]} brackets are common in referencing WKT,
* while the {@code (…)} brackets are common in geometry WKT.</span></td>
* </tr>
* <tr>
* <td>Quote symbols:</td>
* <td>{@code "}…{@code "}</td>
* <td><span style="font-size: small"><b>Note:</b> Apache SIS accepts also {@code “…”} quotes
* for more readable {@code String} literals in Java code, but this is non-standard.</span></td>
* </tr>
* <tr>
* <td>Sequence symbols:</td>
* <td><code>{</code>…<code>}</code></td>
* <td></td>
* </tr>
* <tr>
* <td>Separator:</td>
* <td>{@code ,}</td>
* <td></td>
* </tr>
* </table>
*
* Users can create their own {@code Symbols} instance for parsing or formatting a WKT with different symbols.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.0
*
* @see WKTFormat#getSymbols()
* @see WKTFormat#setSymbols(Symbols)
*
* @since 0.4
* @module
*/
public class Symbols implements Localized, Cloneable, Serializable {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -1730166945430878916L;
/**
* Set to {@code true} if parsing and formatting of numbers in scientific notation is allowed.
* The way to achieve that is currently a hack, because {@link NumberFormat} has no API for
* managing that as of JDK 1.8.
*
* @todo See if a future version of JDK allows us to get ride of this ugly hack.
*/
@Workaround(library = "JDK", version = "1.8")
static final boolean SCIENTIFIC_NOTATION = true;
/**
* Separator between numbers in a sequence of numbers.
* This is used for example between the coordinates of a point.
*/
static final char NUMBER_SEPARATOR = ' ';
/**
* The prefix character for the value of a WKT fragment.
*/
static final char FRAGMENT_VALUE = '$';
/**
* A set of symbols with values between square brackets, like {@code DATUM["WGS84"]}.
* This instance defines:
*
* <ul>
* <li>{@link Locale#ROOT} for {@linkplain java.text.DecimalFormatSymbols decimal format symbols}.</li>
* <li>Square brackets by default, as in {@code DATUM["WGS84"]}, but accepting also curly brackets as in
* {@code DATUM("WGS84")}. Both are legal WKT.</li>
* <li>English quotation mark ({@code '"'}) by default, but accepting also “…” quotes
* for more readable {@link String} constants in Java code.</li>
* <li>Coma separator followed by a space ({@code ", "}).</li>
* </ul>
*
* This is the most frequently used WKT format for referencing objects.
*/
public static final Symbols SQUARE_BRACKETS = new Symbols(
new int[] {'[', ']', '(', ')'}, new int[] {'"', '"', '“', '”'});
/**
* A set of symbols with values between parentheses, like {@code DATUM("WGS84")}.
* This instance is identical to {@link #SQUARE_BRACKETS} except that the default
* brackets are the curly ones instead than the square ones (but both are still
* accepted at parsing time).
*
* <p>This format is rare with referencing objects but common with geometry objects.</p>
*/
public static final Symbols CURLY_BRACKETS = new Symbols(
new int[] {'(', ')', '[', ']'}, SQUARE_BRACKETS.quotes);
/**
* The locale of {@linkplain java.text.DecimalFormatSymbols decimal format symbols} or other symbols.
*
* @see #getLocale()
*/
private Locale locale;
/**
* List of characters (as Unicode code points) acceptable as opening or closing brackets.
* The array shall comply to the following restrictions:
*
* <ul>
* <li>The characters at index 0 and 1 are the preferred opening and closing brackets respectively.</li>
* <li>For each even index <var>i</var>, {@code brackets[i+1]} is the closing bracket matching {@code brackets[i]}.</li>
* </ul>
*
* @see #getOpeningBracket(int)
* @see #getClosingBracket(int)
*/
private int[] brackets;
/**
* List of characters (as Unicode code point) used for opening or closing a quoted text.
* The array shall comply to the following restrictions:
*
* <ul>
* <li>The characters at index 0 and 1 are the preferred opening and closing quotes respectively.</li>
* <li>For each even index <var>i</var>, {@code quotes[i+1]} is the closing quote matching {@code quotes[i]}.</li>
* </ul>
*
* Both opening and closing quotes are usually {@code '"'}.
*/
private int[] quotes;
/**
* The preferred closing quote character ({@code quotes[1]}) as a string.
* We use the closing quote because this is the character that the parser
* will look for determining the text end.
*
* @see #getQuote()
* @see #readResolve()
*/
private transient String quote;
/**
* The character (as Unicode code point) used for opening ({@code openSequence})
* or closing ({@code closeSequence}) an array or enumeration.
*/
private int openSequence, closeSequence;
/**
* The string used as a separator in a list of values. This is usually {@code ", "},
* but may be different if a non-English locale is used for formatting numbers.
*/
private String separator;
/**
* Same value than {@link #separator} but without leading and trailing spaces.
*/
private transient String trimmedSeparator;
/**
* {@code true} if this instance shall be considered as immutable.
*/
private boolean isImmutable;
/**
* Creates a new set of WKT symbols initialized to a copy of the given symbols.
*
* @param symbols the symbols to copy.
*/
public Symbols(final Symbols symbols) {
ensureNonNull("symbols", symbols);
locale = symbols.locale;
brackets = symbols.brackets;
quotes = symbols.quotes;
quote = symbols.quote;
openSequence = symbols.openSequence;
closeSequence = symbols.closeSequence;
separator = symbols.separator;
trimmedSeparator = symbols.trimmedSeparator;
}
/**
* Constructor reserved to {@link #SQUARE_BRACKETS} and {@link #CURLY_BRACKETS} constants.
* The given array is stored by reference - it is not cloned.
*/
private Symbols(final int[] brackets, final int[] quotes) {
this.locale = Locale.ROOT;
this.brackets = brackets;
this.quotes = quotes;
this.quote = "\"";
this.openSequence = '{';
this.closeSequence = '}';
this.separator = ", ";
this.trimmedSeparator = ",";
this.isImmutable = true;
}
/**
* Throws an exception if this set of symbols is immutable.
*/
final void checkWritePermission() throws UnsupportedOperationException {
if (isImmutable) {
throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, "Symbols"));
}
}
/**
* Returns the default set of symbols.
* This is currently set to {@link #SQUARE_BRACKETS}.
*
* @return the default set of symbols.
*/
public static Symbols getDefault() {
return SQUARE_BRACKETS;
}
/**
* Returns the locale for formatting dates and numbers.
* The default value is {@link Locale#ROOT}.
*
* <div class="section">Relationship between {@code Symbols} locale and {@code WKTFormat} locale</div>
* The {@code WKTFormat.getLocale(Locale.DISPLAY)} property specifies the language to use when
* formatting {@link org.opengis.util.InternationalString} instances and can be set to any value.
* On the contrary, the {@code Locale} property of this {@code Symbols} class controls
* the decimal format symbols and is very rarely set to an other locale than {@code Locale.ROOT}.
*
* @return the locale for dates and numbers.
*
* @see WKTFormat#getLocale(Locale.Category)
*/
@Override
public final Locale getLocale() {
return locale;
}
/**
* Sets the locale of decimal format symbols or other symbols.
* Note that any non-English locale is likely to produce WKT that do not conform to ISO 19162.
* Such WKT can be used for human reading, but not for data export.
*
* @param locale the new symbols locale.
*/
public void setLocale(final Locale locale) {
checkWritePermission();
ensureNonNull("locale", locale);
this.locale = locale;
}
/**
* Implementation of {@link #matchingBracket(int)} and {@link #matchingQuote(int)}.
*/
private static int matching(final int[] chars, final int c) {
for (int i = 0; i < chars.length; i += 2) {
if (chars[i] == c) {
return chars[i + 1];
}
}
return -1;
}
/**
* If the given character is an opening bracket, returns the matching closing bracket.
* Otherwise returns -1.
*/
final int matchingBracket(final int c) {
return matching(brackets, c);
}
/**
* Returns the number of paired brackets. For example if the WKT parser accepts both the
* {@code […]} and {@code (…)} bracket pairs, then this method returns 2.
*
* @return the number of bracket pairs.
*
* @see #getOpeningBracket(int)
* @see #getClosingBracket(int)
*/
public final int getNumPairedBrackets() {
return brackets.length >>> 1;
}
/**
* Returns the opening bracket character at the given index.
* Index 0 stands for the default bracket used at formatting time.
* All other index are for optional brackets accepted at parsing time.
*
* @param index index of the opening bracket to get, from 0 to {@link #getNumPairedBrackets()} exclusive.
* @return the opening bracket at the given index, as a Unicode code point.
* @throws IndexOutOfBoundsException if the given index is out of bounds.
*/
public final int getOpeningBracket(final int index) {
return brackets[index << 1];
}
/**
* Returns the closing bracket character at the given index.
* Index 0 stands for the default bracket used at formatting time.
* All other index are for optional brackets accepted at parsing time.
*
* @param index index of the closing bracket to get, from 0 to {@link #getNumPairedBrackets()} exclusive.
* @return the closing bracket at the given index, as a Unicode code point.
* @throws IndexOutOfBoundsException if the given index is out of bounds.
*/
public final int getClosingBracket(final int index) {
return brackets[(index << 1) | 1];
}
/**
* Sets the opening and closing brackets to the given pairs.
* Each string shall contain exactly two code points (usually two characters).
* The first code point is taken as the opening bracket, and the second code point as the closing bracket.
*
* <div class="note"><b>Example:</b>
* The following code will instruct the WKT formatter to use the (…) pair of brackets at formatting time,
* but still accept the more common […] pair of brackets at parsing time:
*
* {@preformat java
* symbols.setPairedBrackets("()", "[]");
* }</div>
*
* @param preferred the preferred pair of opening and closing quotes, used at formatting time.
* @param alternatives alternative pairs of opening and closing quotes accepted at parsing time.
*/
public void setPairedBrackets(final String preferred, final String... alternatives) {
checkWritePermission();
brackets = toCodePoints(preferred, alternatives);
}
/**
* If the given character is an opening quote, returns the matching closing quote.
* Otherwise returns -1.
*/
final int matchingQuote(final int c) {
return matching(quotes, c);
}
/**
* Returns the number of paired quotes. For example if the WKT parser accepts both the
* {@code "…"} and {@code “…”} quote pairs, then this method returns 2.
*
* @return the number of quote pairs.
*
* @see #getOpeningQuote(int)
* @see #getClosingQuote(int)
*/
public final int getNumPairedQuotes() {
return quotes.length >>> 1;
}
/**
* Returns the opening quote character at the given index.
* Index 0 stands for the default quote used at formatting time, which is usually {@code '"'}.
* All other index are for optional quotes accepted at parsing time.
*
* @param index index of the opening quote to get, from 0 to {@link #getNumPairedQuotes()} exclusive.
* @return the opening quote at the given index, as a Unicode code point.
* @throws IndexOutOfBoundsException if the given index is out of bounds.
*/
public final int getOpeningQuote(final int index) {
return quotes[index << 1];
}
/**
* Returns the closing quote character at the given index.
* Index 0 stands for the default quote used at formatting time, which is usually {@code '"'}.
* All other index are for optional quotes accepted at parsing time.
*
* @param index index of the closing quote to get, from 0 to {@link #getNumPairedQuotes()} exclusive.
* @return the closing quote at the given index, as a Unicode code point.
* @throws IndexOutOfBoundsException if the given index is out of bounds.
*/
public final int getClosingQuote(final int index) {
return quotes[(index << 1) | 1];
}
/**
* Returns the preferred closing quote character as a string. This is the quote to double if it
* appears in a Unicode string to format. We check for the closing quote because this is the one
* that the parser will look for determining the text end.
*/
final String getQuote() {
return quote;
}
/**
* Sets the opening and closing quotes to the given pairs.
* Each string shall contain exactly two code points (usually two characters).
* The first code point is taken as the opening quote, and the second code point as the closing quote.
*
* <div class="note"><b>Example:</b>
* The following code will instruct the WKT formatter to use the prettier “…” quotation marks at formatting time
* (especially useful for {@code String} constants in Java code), but still accept the standard "…" quotation marks
* at parsing time:
*
* {@preformat java
* symbols.setPairedQuotes("“”", "\"\"");
* }</div>
*
* @param preferred the preferred pair of opening and closing quotes, used at formatting time.
* @param alternatives alternative pairs of opening and closing quotes accepted at parsing time.
*/
public void setPairedQuotes(final String preferred, final String... alternatives) {
checkWritePermission();
quotes = toCodePoints(preferred, alternatives);
quote = preferred.substring(Character.charCount(quotes[0])).trim();
}
/**
* Packs the given pairs of bracket or quotes in a single array of code points.
* This method also verifies arguments validity.
*/
private static int[] toCodePoints(final String preferred, final String[] alternatives) {
ensureNonEmpty("preferred", preferred);
final int n = (alternatives != null) ? alternatives.length : 0;
final int[] array = new int[(n+1) * 2];
String name = "preferred";
String pair = preferred;
int i=0, j=0;
while (true) {
if (pair.codePointCount(0, pair.length()) != 2) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, name, pair));
}
final int c = pair.codePointAt(0);
ensureValidQuoteOrBracket(name, array[j++] = c);
ensureValidQuoteOrBracket(name, array[j++] = pair.codePointAt(Character.charCount(c)));
if (i >= n) {
break;
}
ensureNonNullElement(name = "alternatives", i, pair = alternatives[i++]);
}
return array;
}
/**
* Ensures that the given code point is a valid Unicode code point but not a Unicode identifier part.
*/
private static void ensureValidQuoteOrBracket(final String name, final int code) {
ensureValidUnicodeCodePoint(name, code);
if (Character.isUnicodeIdentifierPart(code) || Character.isSpaceChar(code) || code == FRAGMENT_VALUE) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalCharacter_2,
name, String.valueOf(Character.toChars(code))));
}
}
/**
* Returns the character used for opening a sequence of values.
* This is usually <code>'{'</code>.
*
* @return the character used for opening a sequence of values, as a Unicode code point.
*/
public final int getOpenSequence() {
return openSequence;
}
/**
* Returns the character used for closing a sequence of values.
* This is usually <code>'}'</code>.
*
* @return the character used for closing a sequence of values, as a Unicode code point.
*/
public final int getCloseSequence() {
return closeSequence;
}
/**
* Sets the characters used for opening and closing a sequence of values.
*
* @param openSequence the character for opening a sequence of values, as a Unicode code point.
* @param closeSequence the character for closing a sequence of values, as a Unicode code point.
*/
public void setSequenceBrackets(final int openSequence, final int closeSequence) {
checkWritePermission();
ensureValidQuoteOrBracket("openSequence", openSequence);
ensureValidQuoteOrBracket("closeSequence", closeSequence);
this.openSequence = openSequence;
this.closeSequence = closeSequence;
}
/**
* Returns the string used as a separator in a list of values. This is usually {@code ", "},
* but may be different if a non-English locale is used for formatting numbers.
*
* @return the string used as a separator in a list of values.
*/
public final String getSeparator() {
return separator;
}
/**
* Sets the string to use as a separator in a list of values.
* The given string will be used "as-is" at formatting time,
* but leading and trailing spaces will be ignored at parsing time.
*
* @param separator the new string to use as a separator in a list of values.
*/
public void setSeparator(final String separator) {
checkWritePermission();
final String s = CharSequences.trimWhitespaces(separator.trim());
ensureNonEmpty("separator", s);
this.separator = separator;
trimmedSeparator = s;
}
/**
* Returns the separator without trailing spaces.
*/
final String trimmedSeparator() {
return trimmedSeparator;
}
/**
* Returns the value of {@link #getSeparator()} without trailing spaces,
* followed by the system line separator.
*/
final String separatorNewLine() {
final String separator = getSeparator();
return separator.substring(0, CharSequences.skipTrailingWhitespaces(separator, 0, separator.length()))
.concat(System.lineSeparator());
}
/**
* Creates a new number format to use for parsing and formatting. Each {@link WKTFormat} will
* create its own instance, since {@link NumberFormat}s are not guaranteed to be thread-safe.
*
* <div class="section">Scientific notation</div>
* The {@link NumberFormat} created here does not use scientific notation. This is okay for many
* WKT formatting purpose since Earth ellipsoid axis lengths in metres are large enough for trigging
* scientific notation, while we want to express them as normal numbers with centimetre precision.
* However this is problematic for small numbers like 1E-5. Callers may need to adjust the precision
* depending on the kind of numbers (length or angle) to format.
*/
final NumberFormat createNumberFormat() {
final NumberFormat format = NumberFormat.getNumberInstance(locale);
format.setGroupingUsed(false);
return format;
}
/**
* Returns {@code true} if the formatter should use scientific notation for the given value.
* We use scientific notation if the number magnitude is too high or too low. The threshold values used here
* may be different than the threshold values used in the standard {@link StringBuilder#append(double)} method.
* In particular, we use a higher threshold for large numbers because ellipsoid axis lengths are above the JDK
* threshold when the axis length is given in feet (about 2.1E+7) while we still want to format them as usual numbers.
*
* Note that we perform this special formatting only if the 'NumberFormat' is not localized (which is the usual case).
*
* @param abs the absolute value of the number to format.
*/
final boolean useScientificNotation(final double abs) {
return SCIENTIFIC_NOTATION && (abs < 1E-3 || abs >= 1E+9) && locale == Locale.ROOT;
}
/**
* Returns {@code true} if the given WKT contains at least one instance of the given element.
* Invoking this method is equivalent to invoking {@link String#contains(CharSequence)} except
* for the following:
*
* <ul>
* <li>The search is case-insensitive.</li>
* <li>Characters between {@linkplain #getOpeningQuote(int) opening quotes} and
* {@linkplain #getClosingQuote(int) closing quotes} are ignored.</li>
* <li>The element found in the given WKT can not be preceded by other
* {@linkplain Character#isUnicodeIdentifierPart(int) Unicode identifier characters}.</li>
* <li>The element found in the given WKT must be followed, ignoring space, by an
* {@linkplain #getOpeningBracket(int) opening bracket}.</li>
* </ul>
*
* The purpose of this method is to guess some characteristics about the encoded object without
* the cost of a full WKT parsing.
*
* @param wkt the WKT to inspect.
* @param element the element to search for.
* @return {@code true} if the given WKT contains at least one instance of the given element.
*/
public boolean containsElement(final CharSequence wkt, final String element) {
ensureNonNull("wkt", wkt);
ensureNonEmpty("element", element);
if (!CharSequences.isUnicodeIdentifier(element)) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.NotAUnicodeIdentifier_1, element));
}
return containsElement(wkt, element, 0);
}
/**
* Returns {@code true} if the given WKT contains at least one instance of the {@code AXIS[…]} element.
* Invoking this method is equivalent to invoking
* <code>{@linkplain #containsElement(CharSequence, String) containsElement}(wkt, "AXIS")</code>.
*
* <div class="section">Use case</div>
* The check for axis elements is of particular interest because the axis order is a frequent cause
* of confusion when processing geographic data. Some applications just ignore any declared axis order
* in favor of their own hard-coded (<var>longitude</var>, <var>latitude</var>) axis order.
* Consequently, the presence of {@code AXIS[…]} elements in a WKT is an indication that the encoded
* object may not be understood as intended by some external software products.
*
* @param wkt the WKT to inspect.
* @return {@code true} if the given WKT contains at least one instance of the {@code AXIS[…]} element.
*/
public boolean containsAxis(final CharSequence wkt) {
ensureNonNull("wkt", wkt);
return containsElement(wkt, "AXIS", 0);
}
/**
* Implementation of {@link #containsElement(CharSequence, String)} without verification of argument validity.
*
* @param wkt the WKT to inspect.
* @param element the element to search. Must contains only uppercase letters.
* @param offset the index to start the search from.
*/
private boolean containsElement(final CharSequence wkt, final String element, int offset) {
final int[] quotes = this.quotes;
final int length = wkt.length();
boolean isQuoting = false;
int closeQuote = 0;
while (offset < length) {
int c = Character.codePointAt(wkt, offset);
if (closeQuote != 0) {
if (c == closeQuote) {
isQuoting = false;
}
} else for (int i=0; i<quotes.length; i+=2) {
if (c == quotes[i]) {
closeQuote = quotes[i | 1];
isQuoting = true;
break;
}
}
if (!isQuoting && Character.isUnicodeIdentifierStart(c)) {
/*
* Found the beginning of a Unicode identifier.
* Check if this is the identifier we were looking for.
*/
if (CharSequences.regionMatches(wkt, offset, element, true)) {
offset = CharSequences.skipLeadingWhitespaces(wkt, offset + element.length(), length);
if (offset >= length) {
break;
}
c = Character.codePointAt(wkt, offset);
if (matchingBracket(c) >= 0) {
return true;
}
} else {
/*
* Not the identifier we were looking for. Skip the whole identifier.
*/
do {
offset += Character.charCount(c);
if (offset >= length) {
return false;
}
c = Character.codePointAt(wkt, offset);
} while (Character.isUnicodeIdentifierPart(c));
}
}
offset += Character.charCount(c);
}
return false;
}
/**
* Returns an immutable copy of this set of symbols, or {@code this} if this instance is already immutable.
*/
final Symbols immutable() {
if (isImmutable) {
return this;
}
final Symbols clone = clone();
clone.isImmutable = true;
return clone;
}
/**
* Returns a clone of this {@code Symbols}.
*
* @return a clone of this {@code Symbols}.
*/
@Override
public Symbols clone() {
final Symbols clone;
try {
clone = (Symbols) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
/*
* No needs to copy the arrays, because their content are never modified.
* Instead, the setter methods create new arrays.
*/
clone.isImmutable = false;
return clone;
}
/**
* Compares this {@code Symbols} with the given object for equality.
*
* @param other the object to compare with this {@code Symbols}.
* @return {@code true} if both objects are equal.
*/
@Override
public boolean equals(final Object other) {
if (other instanceof Symbols) {
final Symbols that = (Symbols) other;
return Arrays.equals(brackets, that.brackets) &&
Arrays.equals(quotes, that.quotes) &&
// no need to compare 'quote' because it is computed from 'quotes'.
openSequence == that.openSequence &&
closeSequence == that.closeSequence &&
separator.equals(that.separator) &&
locale.equals(that.locale);
}
return false;
}
/**
* Returns a hash code value for this object.
*
* @return a hash code value.
*/
@Override
public int hashCode() {
return Arrays.deepHashCode(new Object[] {brackets, quotes, openSequence, closeSequence, separator, locale});
}
/**
* Invoked on deserialization for replacing the deserialized instance by the constant instance.
* This method also opportunistically recompute the {@link #quote} field if no replacement is done.
*
* @return the object to use after deserialization.
* @throws ObjectStreamException required by specification but should never be thrown.
*/
final Object readResolve() throws ObjectStreamException {
if (isImmutable) {
if (equals(SQUARE_BRACKETS)) return SQUARE_BRACKETS;
if (equals(CURLY_BRACKETS)) return CURLY_BRACKETS;
}
quote = String.valueOf(Character.toChars(quotes[1]));
trimmedSeparator = CharSequences.trimWhitespaces(separator.trim());
return this;
}
}