blob: 94d925e3292f38f5c39783d7ea723fe6fcbf759e [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.xml;
import java.net.URI;
import java.net.URL;
import java.net.URISyntaxException;
import java.net.MalformedURLException;
import java.util.MissingResourceException;
import java.util.IllformedLocaleException;
import java.util.Locale;
import java.util.UUID;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import javax.measure.Unit;
import javax.measure.format.ParserException;
import org.apache.sis.measure.Units;
import org.apache.sis.util.Locales;
import static org.apache.sis.util.CharSequences.trimWhitespaces;
/**
* Performs conversions of XML element or attribute values encountered during XML (un)marshalling.
* Each method in this class is a converter and can be invoked at (un)marshalling time.
* The default implementation is straightforward and documented in the javadoc of each method.
*
* <p>This class provides a way to handle the errors which may exist in some XML documents.
* For example a URL in the document may be malformed, causing a {@link MalformedURLException}
* to be thrown. If this error is not handled, it will cause the (un)marshalling of the entire
* document to fail. An application may want to change this behavior by replacing URLs that
* are known to be erroneous by fixed versions of those URLs. Example:</p>
*
* {@preformat java
* class URLFixer extends ValueConverter {
* &#64;Override
* public URL toURL(MarshalContext context, URI uri) throws MalformedURLException {
* try {
* return super.toURL(context, uri);
* } catch (MalformedURLException e) {
* if (uri.equals(KNOWN_ERRONEOUS_URI) {
* return FIXED_URL;
* } else {
* throw e;
* }
* }
* }
* }
* }
*
* See the {@link XML#CONVERTER} javadoc for an example of registering a custom
* {@code ValueConverter} to a (un)marshaller.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.5
* @since 0.3
* @module
*/
public class ValueConverter {
/**
* The default, thread-safe and immutable instance. This instance defines the
* converters used during every (un)marshalling if no {@code ValueConverter}
* was explicitly set.
*/
public static final ValueConverter DEFAULT = new ValueConverter();
/**
* Creates a default {@code ValueConverter}. This is for subclasses only,
* since new instances are useful only if at least one method is overridden.
*/
protected ValueConverter() {
}
/**
* Invoked when an exception occurred in any {@code toXXX(…)} method. The default implementation
* does nothing and return {@code false}, which will cause the (un)marshalling process of the
* whole XML document to fail.
*
* <p>This method provides a single hook that subclasses can override in order to provide their
* own error handling for every methods defined in this class, like the example documented in
* the {@link XML#CONVERTER} javadoc. Subclasses also have the possibility to override individual
* {@code toXXX(…)} methods, like the example provided in this <a href="#skip-navbar_top">class
* javadoc</a>.</p>
*
* @param <T> the compile-time type of the {@code sourceType} argument.
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the value that can't be converted.
* @param sourceType the base type of the value to convert. This is determined by the argument type of the method
* that caught the exception. For example the source type is always {@code URI.class}
* if the exception has been caught by the {@link #toURL(MarshalContext, URI)} method.
* @param targetType the expected type of the converted object.
* @param exception the exception that occurred during the conversion attempt.
* @return {@code true} if the (un)marshalling process should continue despite this error,
* or {@code false} (the default) if the exception should be propagated, thus causing
* the (un)marshalling to fail.
*/
protected <T> boolean exceptionOccured(MarshalContext context, T value,
Class<T> sourceType, Class<?> targetType, Exception exception)
{
return false;
}
/**
* Converts the given locale to a language code. For better compliance with ISO standards, the language code
* should be a 3-letters ISO 639-2 code (e.g. {@code "jpn"} for {@linkplain Locale#JAPANESE Japanese}).
* However those codes may not be available for every locales.
*
* <p>The default implementation performs the following steps:</p>
* <ul>
* <li>Try {@link Locale#getISO3Language()}:<ul>
* <li>On success, return that value if non-empty, or {@code null} otherwise.</li>
* <li>If an exception has been thrown, then:<ul>
* <li>If {@link #exceptionOccured exceptionOccured(…)} return {@code true}, then
* returns {@code value.getLanguage()} if non-empty or {@code null} otherwise.</li>
* <li>If {@code exceptionOccured(…)} returned {@code false} (which is the default
* behavior), then let the exception propagate.</li>
* </ul></li>
* </ul></li>
* </ul>
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the locale to convert to a language code, or {@code null}.
* @return the language code, or {@code null} if the given value was null or does not contains a language code.
* @throws MissingResourceException if no language code can be found for the given locale.
*
* @see Locale#getISO3Language()
* @see Locale#getLanguage()
*/
public String toLanguageCode(final MarshalContext context, final Locale value) throws MissingResourceException {
if (value != null) {
String code;
try {
code = value.getISO3Language();
} catch (MissingResourceException e) {
if (!exceptionOccured(context, value, Locale.class, String.class, e)) {
throw e;
}
code = value.getLanguage();
}
if (!code.isEmpty()) {
return code;
}
}
return null;
}
/**
* Converts the given locale to a country code. For better compliance with ISO standards, the country code
* should be a 2-letters ISO 3166 code (e.g. {@code "JP"} for {@linkplain Locale#JAPAN Japan}).
*
* <p>The default implementation returns {@link Locale#getCountry()} if non-empty, or {@code null} otherwise.</p>
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the locale to convert to a country code, or {@code null}.
* @return the country code, or {@code null} if the given value was null or does not contains a country code.
* @throws MissingResourceException if no country code can be found for the given locale.
*
* @see Locale#getISO3Country()
* @see Locale#getCountry()
*/
public String toCountryCode(final MarshalContext context, final Locale value) throws MissingResourceException {
if (value != null) {
String code = value.getCountry();
if (!code.isEmpty()) {
return code;
}
}
return null;
}
/**
* Converts the given character set to a code.
*
* <p>The default implementation first invokes {@link Charset#name()}. Then if marshalling to legacy ISO 19139:2007,
* this method converts the <a href="http://www.iana.org/assignments/character-sets">IANA</a> name to a
* ISO 19115:2003 {@code MD_CharacterSetCode} using the following equivalence table:</p>
*
* <table class="sis">
* <caption>IANA to ISO 19115:2003 character set code</caption>
* <tr>
* <td><table class="compact">
* <caption>From ISO codes</caption>
* <tr><td style="width: 90px"><b>IANA</b></td><td><b>ISO 19115:2003</b></td></tr>
* <tr><td>{@code ISO-8859-1}</td> <td>{@code 8859part1}</td></tr>
* <tr><td>{@code ISO-8859-2}</td> <td>{@code 8859part2}</td></tr>
* <tr><td>{@code ISO-8859-3}</td> <td>{@code 8859part3}</td></tr>
* <tr><td>{@code ISO-8859-4}</td> <td>{@code 8859part4}</td></tr>
* <tr><td>{@code ISO-8859-5}</td> <td>{@code 8859part5}</td></tr>
* <tr><td>{@code ISO-8859-6}</td> <td>{@code 8859part6}</td></tr>
* <tr><td>{@code ISO-8859-7}</td> <td>{@code 8859part7}</td></tr>
* <tr><td>{@code ISO-8859-8}</td> <td>{@code 8859part8}</td></tr>
* <tr><td>{@code ISO-8859-9}</td> <td>{@code 8859part9}</td></tr>
* <tr><td>{@code ISO-8859-10}</td> <td>{@code 8859part10}</td></tr>
* <tr><td>{@code ISO-8859-11}</td> <td>{@code 8859part11}</td></tr>
* <tr><td>{@code ISO-8859-12}</td> <td>{@code 8859part12}</td></tr>
* <tr><td>{@code ISO-8859-13}</td> <td>{@code 8859part13}</td></tr>
* <tr><td>{@code ISO-8859-14}</td> <td>{@code 8859part14}</td></tr>
* <tr><td>{@code ISO-8859-15}</td> <td>{@code 8859part15}</td></tr>
* <tr><td>{@code ISO-8859-16}</td> <td>{@code 8859part16}</td></tr>
* </table></td>
* <td class="sep"><table class="compact">
* <caption>Others</caption>
* <tr><td style="width: 90px"><b>IANA</b></td><td><b>ISO 19115:2003</b></td></tr>
* <tr><td>{@code UCS-2}</td> <td>{@code ucs2}</td></tr>
* <tr><td>{@code UCS-4}</td> <td>{@code ucs4}</td></tr>
* <tr><td>{@code UTF-7}</td> <td>{@code utf7}</td></tr>
* <tr><td>{@code UTF-8}</td> <td>{@code utf8}</td></tr>
* <tr><td>{@code UTF-16}</td> <td>{@code utf16}</td></tr>
* <tr><td>{@code JIS_X0201}</td> <td>{@code jis}</td></tr>
* <tr><td>{@code Shift_JIS}</td> <td>{@code shiftJIS}</td></tr>
* <tr><td>{@code EUC-JP}</td> <td>{@code eucJP}</td></tr>
* <tr><td>{@code US-ASCII}</td> <td>{@code usAscii}</td></tr>
* <tr><td>{@code EBCDIC}</td> <td>{@code ebcdic}</td></tr>
* <tr><td>{@code EUC-KR}</td> <td>{@code eucKR}</td></tr>
* <tr><td>{@code Big5}</td> <td>{@code big5}</td></tr>
* <tr><td>{@code GB2312}</td> <td>{@code GB2312}</td></tr>
* </table></td>
* </tr>
* </table>
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the locale to convert to a character set code, or {@code null}.
* @return the country code, or {@code null} if the given value was null.
*
* @see Charset#name()
*
* @since 0.5
*/
public String toCharsetCode(final MarshalContext context, final Charset value) {
if (value != null) {
return LegacyCodes.fromIANA(value.name());
}
return null;
}
/**
* Converts the given string to a locale. The string is the language code either as the 2
* letters or the 3 letters ISO code. It can optionally be followed by the {@code '_'}
* character and the country code (again either as 2 or 3 letters), optionally followed
* by {@code '_'} and the variant.
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the string to convert to a locale, or {@code null}.
* @return the converted locale, or {@code null} if the given value was null or empty, or
* if an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
* @throws IllformedLocaleException if the given string can not be converted to a locale.
*
* @see Locales#parse(String)
*/
public Locale toLocale(final MarshalContext context, String value) throws IllformedLocaleException {
value = trimWhitespaces(value);
if (value != null && !value.isEmpty()) try {
return Locales.parse(value);
} catch (IllformedLocaleException e) {
if (!exceptionOccured(context, value, String.class, Locale.class, e)) {
throw e;
}
}
return null;
}
/**
* Converts the given string to a character set. The string can be either a
* <a href="http://www.iana.org/assignments/character-sets">IANA</a> identifier,
* or one of the ISO 19115:2003 {@code MD_CharacterSetCode} identifier.
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the string to convert to a character set, or {@code null}.
* @return the converted character set, or {@code null} if the given value was null or empty, or
* if an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
* @throws IllegalCharsetNameException if the given string can not be converted to a character set.
*
* @see Charset#forName(String)
*
* @since 0.5
*/
public Charset toCharset(final MarshalContext context, String value) throws IllegalCharsetNameException {
value = trimWhitespaces(value);
if (value != null && !value.isEmpty()) {
value = LegacyCodes.toIANA(value);
try {
return Charset.forName(value);
} catch (IllegalCharsetNameException e) {
if (!exceptionOccured(context, value, String.class, Charset.class, e)) {
throw e;
}
}
}
return null;
}
/**
* Converts the given string to a unit. The default implementation is as below, omitting
* the check for null value and the call to {@link #exceptionOccured exceptionOccured(…)}
* in case of error:
*
* {@preformat java
* return Units.valueOf(value);
* }
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the string to convert to a unit, or {@code null}.
* @return the converted unit, or {@code null} if the given value was null or empty, or
* if an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
* @throws IllegalArgumentException if the given string can not be converted to a unit.
*
* @see Units#valueOf(String)
*/
public Unit<?> toUnit(final MarshalContext context, String value) throws IllegalArgumentException {
value = trimWhitespaces(value);
if (value != null && !value.isEmpty()) try {
return Units.valueOf(value);
} catch (ParserException e) {
if (!exceptionOccured(context, value, String.class, Unit.class, e)) {
throw e;
}
}
return null;
}
/**
* Converts the given string to a Universal Unique Identifier. The default implementation
* is as below, omitting the check for null value and the call to {@link #exceptionOccured
* exceptionOccured(…)} in case of error:
*
* {@preformat java
* return UUID.fromString(value);
* }
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the string to convert to a UUID, or {@code null}.
* @return the converted UUID, or {@code null} if the given value was null or empty, or
* if an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
* @throws IllegalArgumentException if the given string can not be converted to a UUID.
*
* @see UUID#fromString(String)
*/
public UUID toUUID(final MarshalContext context, String value) throws IllegalArgumentException {
value = trimWhitespaces(value);
if (value != null && !value.isEmpty()) try {
return UUID.fromString(value);
} catch (IllegalArgumentException e) {
if (!exceptionOccured(context, value, String.class, UUID.class, e)) {
throw e;
}
}
return null;
}
/**
* Converts the given string to a URI. The default performs the following work
* (omitting the check for null value and the call to {@link #exceptionOccured
* exceptionOccured(…)} in case of error):
*
* {@preformat java
* return new URI(value);
* }
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the string to convert to a URI, or {@code null}.
* @return the converted URI, or {@code null} if the given value was null or empty, or if
* an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
* @throws URISyntaxException if the given string can not be converted to a URI.
*
* @see URI#URI(String)
*/
public URI toURI(final MarshalContext context, String value) throws URISyntaxException {
value = trimWhitespaces(value);
if (value != null && !value.isEmpty()) try {
return new URI(value);
} catch (URISyntaxException e) {
if (!exceptionOccured(context, value, String.class, URI.class, e)) {
throw e;
}
}
return null;
}
/**
* Converts the given URL to a URI. The default implementation is as below, omitting
* the check for null value and the call to {@link #exceptionOccured exceptionOccured(…)}
* in case of error:
*
* {@preformat java
* return value.toURI();
* }
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the URL to convert to a URI, or {@code null}.
* @return the converted URI, or {@code null} if the given value was null or if an
* exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
* @throws URISyntaxException if the given URL can not be converted to a URI.
*
* @see URL#toURI()
*/
public URI toURI(final MarshalContext context, final URL value) throws URISyntaxException {
if (value != null) try {
return value.toURI();
} catch (URISyntaxException e) {
if (!exceptionOccured(context, value, URL.class, URI.class, e)) {
throw e;
}
}
return null;
}
/**
* Converts the given URI to a URL. The default implementation is as below, omitting
* the check for null value and the call to {@link #exceptionOccured exceptionOccured(…)}
* in case of error:
*
* {@preformat java
* return value.toURL();
* }
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the URI to convert to a URL, or {@code null}.
* @return the converted URL, or {@code null} if the given value was null or if an
* exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
* @throws MalformedURLException if the given URI can not be converted to a URL.
*
* @see URI#toURL()
*/
public URL toURL(final MarshalContext context, final URI value) throws MalformedURLException {
if (value != null) try {
return value.toURL();
} catch (MalformedURLException | IllegalArgumentException e) {
if (!exceptionOccured(context, value, URI.class, URL.class, e)) {
throw e;
}
}
return null;
}
/**
* Converts the given string to a {@code NilReason}. The default implementation is as below,
* omitting the check for null value and the call to {@link #exceptionOccured exceptionOccured(…)}
* in case of error:
*
* {@preformat java
* return NilReason.valueOf(value);
* }
*
* @param context context (GML version, locale, <i>etc.</i>) of the (un)marshalling process.
* @param value the string to convert to a nil reason, or {@code null}.
* @return the converted nil reason, or {@code null} if the given value was null or empty, or
* if an exception was thrown and {@code exceptionOccured(…)} returned {@code true}.
* @throws URISyntaxException if the given string can not be converted to a nil reason.
*
* @see NilReason#valueOf(String)
*/
public NilReason toNilReason(final MarshalContext context, String value) throws URISyntaxException {
value = trimWhitespaces(value);
if (value != null && !value.isEmpty()) try {
return NilReason.valueOf(value);
} catch (URISyntaxException e) {
if (!exceptionOccured(context, value, String.class, URI.class, e)) {
throw e;
}
}
return null;
}
}