| /* |
| * 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.util.iso; |
| |
| import java.util.Map; |
| import java.util.HashMap; |
| import java.util.SortedMap; |
| import java.util.Locale; |
| import java.util.Properties; |
| import java.util.ResourceBundle; |
| import java.util.MissingResourceException; |
| import java.util.IllformedLocaleException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import org.opengis.annotation.UML; |
| import org.opengis.util.CodeList; |
| import org.opengis.util.InternationalString; |
| import org.apache.sis.util.Static; |
| import org.apache.sis.util.Locales; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.logging.Logging; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.collection.Containers; |
| import org.apache.sis.util.collection.BackingStoreException; |
| import org.apache.sis.internal.util.CodeLists; |
| import org.apache.sis.internal.system.Loggers; |
| |
| |
| /** |
| * Static methods working on GeoAPI types and {@link CodeList} values. |
| * This class provides: |
| * |
| * <ul> |
| * <li>Methods for fetching the ISO name or description of a code list:<ul> |
| * <li>{@link #getStandardName(Class)} for ISO name</li> |
| * <li>{@link #getListName(CodeList)} for ISO name</li> |
| * <li>{@link #getDescription(Class)} for a description</li> |
| * </ul></li> |
| * <li>Methods for fetching the ISO name or description of a code value:<ul> |
| * <li>{@link #getCodeName(CodeList)} for ISO name,</li> |
| * <li>{@link #getCodeTitle(CodeList)} for a label or title</li> |
| * <li>{@link #getDescription(CodeList)} for a more verbose description</li> |
| * </ul></li> |
| * <li>Methods for fetching an instance from a name (converse of above {@code get} methods):<ul> |
| * <li>{@link #forCodeName(Class, String, boolean)}</li> |
| * <li>{@link #forEnumName(Class, String)}</li> |
| * </ul></li> |
| * </ul> |
| * |
| * <div class="section">Substituting a free text by a code list</div> |
| * The ISO standard allows to substitute some character strings in the <cite>"free text"</cite> domain |
| * by a {@link CodeList} value. |
| * |
| * <div class="note"><b>Example:</b> |
| * in the following XML fragment, the {@code <mac:type>} value is normally a {@code <gco:CharacterString>} |
| * but has been replaced by a {@code SensorType} code below: |
| * |
| * {@preformat xml |
| * <mac:MI_Instrument> |
| * <mac:type> |
| * <gmi:MI_SensorTypeCode |
| * codeList="http://standards.iso.org/…snip…/codelists.xml#CI_SensorTypeCode" |
| * codeListValue="RADIOMETER">Radiometer</gmi:MI_SensorTypeCode> |
| * </mac:type> |
| * </mac:MI_Instrument> |
| * } |
| * </div> |
| * |
| * Such substitution can be done with: |
| * |
| * <ul> |
| * <li>{@link #getCodeTitle(CodeList)} for getting the {@link InternationalString} instance |
| * to store in a metadata property.</li> |
| * <li>{@link #forCodeTitle(CharSequence)} for retrieving the {@link CodeList} previously stored as an |
| * {@code InternationalString}.</li> |
| * </ul> |
| * |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| * @version 1.0 |
| * @since 0.3 |
| * @module |
| */ |
| public final class Types extends Static { |
| /** |
| * The separator character between class name and attribute name in resource files. |
| */ |
| private static final char SEPARATOR = '.'; |
| |
| /** |
| * The types for ISO 19115 UML identifiers. The keys are UML identifiers. |
| * Values are either class names as {@link String} objects, or the {@link Class} instances. |
| * This map will be built only when first needed. |
| * |
| * @see #forStandardName(String) |
| */ |
| private static Map<String,Object> typeForNames; |
| |
| /** |
| * Do not allow instantiation of this class. |
| */ |
| private Types() { |
| } |
| |
| /** |
| * Returns the ISO name for the given class, or {@code null} if none. |
| * This method can be used for GeoAPI interfaces or {@link CodeList}. |
| * |
| * <div class="note"><b>Examples:</b> |
| * <ul> |
| * <li><code>getStandardName({@linkplain org.opengis.metadata.citation.Citation}.class)</code> |
| * (an interface) returns {@code "CI_Citation"}.</li> |
| * <li><code>getStandardName({@linkplain org.opengis.referencing.cs.AxisDirection}.class)</code> |
| * (a code list) returns {@code "CS_AxisDirection"}.</li> |
| * </ul> |
| * </div> |
| * |
| * This method looks for the {@link UML} annotation on the given type. It does not search for |
| * parent classes or interfaces if the given type is not directly annotated (i.e. {@code @UML} |
| * annotations are not inherited). If no annotation is found, then this method does not fallback |
| * on the Java name since, as the name implies, this method is about standard names. |
| * |
| * @param type the GeoAPI interface or code list from which to get the ISO name, or {@code null}. |
| * @return the ISO name for the given type, or {@code null} if none or if the given type is {@code null}. |
| * |
| * @see #forStandardName(String) |
| */ |
| public static String getStandardName(final Class<?> type) { |
| if (type != null) { |
| final UML uml = type.getAnnotation(UML.class); |
| if (uml != null) { |
| final String id = uml.identifier(); |
| if (id != null && !id.isEmpty()) { |
| /* |
| * Workaround: I though that annotation strings were interned like any other constants, |
| * but it does not seem to be the case as of JDK7. To verify if this explicit call to |
| * String.intern() is still needed in a future JDK release, see the workaround comment |
| * in the org.apache.sis.metadata.PropertyAccessor.name(…) method. |
| */ |
| return id.intern(); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the ISO classname (if available) or the Java classname (as a fallback) of the given |
| * enumeration or code list value. This method uses the {@link UML} annotation if it exists, or |
| * fallback on the {@linkplain Class#getSimpleName() simple class name} otherwise. |
| * |
| * <div class="note"><b>Examples:</b> |
| * <ul> |
| * <li>{@code getListName(ParameterDirection.IN_OUT)} returns {@code "SV_ParameterDirection"}.</li> |
| * <li>{@code getListName(AxisDirection.NORTH)} returns {@code "CS_AxisDirection"}.</li> |
| * <li>{@code getListName(TopicCategory.INLAND_WATERS)} returns {@code "MD_TopicCategoryCode"}.</li> |
| * <li>{@code getListName(ImagingCondition.BLURRED_IMAGE)} returns {@code "MD_ImagingConditionCode"}.</li> |
| * </ul> |
| * </div> |
| * |
| * @param code the code for which to get the class name, or {@code null}. |
| * @return the ISO (preferred) or Java (fallback) class name, or {@code null} if the given code is null. |
| */ |
| public static String getListName(final CodeList<?> code) { |
| if (code == null) { |
| return null; |
| } |
| final Class<?> type = code.getClass(); |
| final String id = getStandardName(type); |
| return (id != null) ? id : type.getSimpleName(); |
| } |
| |
| /** |
| * Returns the ISO name (if available) or the Java name (as a fallback) of the given enumeration or code list |
| * value. If the value has no {@link UML} identifier, then the programmatic name is used as a fallback. |
| * |
| * <div class="note"><b>Examples:</b> |
| * <ul> |
| * <li>{@code getCodeName(ParameterDirection.IN_OUT)} returns {@code "in/out"}.</li> |
| * <li>{@code getCodeName(AxisDirection.NORTH)} returns {@code "north"}.</li> |
| * <li>{@code getCodeName(TopicCategory.INLAND_WATERS)} returns {@code "inlandWaters"}.</li> |
| * <li>{@code getCodeName(ImagingCondition.BLURRED_IMAGE)} returns {@code "blurredImage"}.</li> |
| * </ul> |
| * </div> |
| * |
| * @param code the code for which to get the name, or {@code null}. |
| * @return the UML identifiers or programmatic name for the given code, or {@code null} if the given code is null. |
| * |
| * @see #getCodeLabel(CodeList) |
| * @see #getCodeTitle(CodeList) |
| * @see #getDescription(CodeList) |
| * @see #forCodeName(Class, String, boolean) |
| */ |
| public static String getCodeName(final CodeList<?> code) { |
| if (code == null) { |
| return null; |
| } |
| final String id = code.identifier(); |
| return (id != null && !id.isEmpty()) ? id : code.name(); |
| } |
| |
| /** |
| * Returns a unlocalized title for the given enumeration or code list value. |
| * This method builds a title using heuristics rules, which should give reasonable |
| * results without the need of resource bundles. For better results, consider using |
| * {@link #getCodeTitle(CodeList)} instead. |
| * |
| * <p>The current heuristic implementation iterates over {@linkplain CodeList#names() all code names}, |
| * selects the longest one excluding the {@linkplain CodeList#name() field name} if possible, then |
| * {@linkplain CharSequences#camelCaseToSentence(CharSequence) makes a sentence} from that name.</p> |
| * |
| * <div class="note"><b>Examples:</b> |
| * <ul> |
| * <li>{@code getCodeLabel(AxisDirection.NORTH)} returns {@code "North"}.</li> |
| * <li>{@code getCodeLabel(TopicCategory.INLAND_WATERS)} returns {@code "Inland waters"}.</li> |
| * <li>{@code getCodeLabel(ImagingCondition.BLURRED_IMAGE)} returns {@code "Blurred image"}.</li> |
| * </ul> |
| * </div> |
| * |
| * @param code the code from which to get a title, or {@code null}. |
| * @return a unlocalized title for the given code, or {@code null} if the given code is null. |
| * |
| * @see #getCodeName(CodeList) |
| * @see #getCodeTitle(CodeList) |
| * @see #getDescription(CodeList) |
| */ |
| public static String getCodeLabel(final CodeList<?> code) { |
| if (code == null) { |
| return null; |
| } |
| String id = code.identifier(); |
| final String name = code.name(); |
| if (id == null) { |
| id = name; |
| } |
| for (final String candidate : code.names()) { |
| if (!candidate.equals(name) && candidate.length() >= id.length()) { |
| id = candidate; |
| } |
| } |
| return CharSequences.camelCaseToSentence(id).toString(); |
| } |
| |
| /** |
| * Returns the title of the given enumeration or code list value. Title are usually much shorter than descriptions. |
| * English titles are often the same than the {@linkplain #getCodeLabel(CodeList) code labels}. |
| * |
| * <p>The code or enumeration value given in argument to this method can be retrieved from the returned title |
| * with the {@link #forCodeTitle(CharSequence)} method. See <cite>Substituting a free text by a code list</cite> |
| * in this class javadoc for more information.</p> |
| * |
| * @param code the code for which to get the title, or {@code null}. |
| * @return the title, or {@code null} if the given code is null. |
| * |
| * @see #getDescription(CodeList) |
| * @see #forCodeTitle(CharSequence) |
| */ |
| public static InternationalString getCodeTitle(final CodeList<?> code) { |
| return (code != null) ? new CodeTitle(code) : null; |
| } |
| |
| /** |
| * Returns the description of the given enumeration or code list value, or {@code null} if none. |
| * For a description of the code list as a whole instead than a particular code, |
| * see {@link Types#getDescription(Class)}. |
| * |
| * @param code the code for which to get the localized description, or {@code null}. |
| * @return the description, or {@code null} if none or if the given code is null. |
| * |
| * @see #getCodeTitle(CodeList) |
| * @see #getDescription(Class) |
| */ |
| public static InternationalString getDescription(final CodeList<?> code) { |
| if (code != null) { |
| final String resources = getResources(code.getClass().getName()); |
| if (resources != null) { |
| return new Description(resources, Description.resourceKey(code)); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns a description for the given class, or {@code null} if none. |
| * This method can be used for GeoAPI interfaces or {@link CodeList}. |
| * |
| * @param type the GeoAPI interface or code list from which to get the description, or {@code null}. |
| * @return the description, or {@code null} if none or if the given type is {@code null}. |
| * |
| * @see #getDescription(CodeList) |
| */ |
| public static InternationalString getDescription(final Class<?> type) { |
| final String name = getStandardName(type); |
| if (name != null) { |
| final String resources = getResources(type.getName()); |
| if (resources != null) { |
| return new Description(resources, name); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Returns a description for the given property, or {@code null} if none. |
| * The given type shall be a GeoAPI interface, and the given property shall |
| * be a UML identifier. If any of the input argument is {@code null}, then |
| * this method returns {@code null}. |
| * |
| * @param type the GeoAPI interface from which to get the description of a property, or {@code null}. |
| * @param property the ISO name of the property for which to get the description, or {@code null}. |
| * @return the description, or {@code null} if none or if the given type or property name is {@code null}. |
| */ |
| public static InternationalString getDescription(final Class<?> type, final String property) { |
| if (property != null) { |
| final String name = getStandardName(type); |
| if (name != null) { |
| final String resources = getResources(type.getName()); |
| if (resources != null) { |
| return new Description(resources, name + SEPARATOR + property); |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * The {@link InternationalString} returned by the {@code Types.getDescription(…)} methods. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 0.3 |
| * @since 0.3 |
| * @module |
| */ |
| private static class Description extends ResourceInternationalString { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = -6202647167398898834L; |
| |
| /** |
| * The class loader to use for fetching GeoAPI resources. |
| * Since the resources are bundled in the GeoAPI JAR file, |
| * we use the instance that loaded GeoAPI for more determinist behavior. |
| */ |
| private static final ClassLoader CLASSLOADER = UML.class.getClassLoader(); |
| |
| /** |
| * Creates a new international string from the specified resource bundle and key. |
| * |
| * @param resources the name of the resource bundle, as a fully qualified class name. |
| * @param key the key for the resource to fetch. |
| */ |
| Description(final String resources, final String key) { |
| super(resources, key); |
| } |
| |
| /** |
| * Loads the resources using the class loader used for loading GeoAPI interfaces. |
| */ |
| @Override |
| protected final ResourceBundle getBundle(final Locale locale) { |
| return ResourceBundle.getBundle(resources, locale, CLASSLOADER); |
| } |
| |
| /** |
| * Returns the description for the given locale, or fallback on a default description |
| * if no resources exist for that locale. |
| */ |
| @Override |
| public final String toString(final Locale locale) { |
| try { |
| return super.toString(locale); |
| } catch (MissingResourceException e) { |
| Logging.ignorableException(Logging.getLogger(Loggers.LOCALIZATION), ResourceInternationalString.class, "toString", e); |
| return fallback(); |
| } |
| } |
| |
| /** |
| * Returns a fallback if no resource is found. |
| */ |
| String fallback() { |
| return CharSequences.camelCaseToSentence(key.substring(key.lastIndexOf(SEPARATOR) + 1)).toString(); |
| } |
| |
| /** |
| * Returns the resource key for the given code list. |
| */ |
| static String resourceKey(final CodeList<?> code) { |
| String key = getCodeName(code); |
| if (key.indexOf(SEPARATOR) < 0) { |
| key = getListName(code) + SEPARATOR + key; |
| } |
| return key; |
| } |
| } |
| |
| /** |
| * The {@link InternationalString} returned by the {@code Types.getCodeTitle(…)} method. |
| * The code below is a duplicated - in a different way - of {@code CodeListUID(CodeList)} |
| * constructor ({@link org.apache.sis.internal.jaxb.code package}). This duplication exists |
| * because {@code CodeListUID} constructor stores more information in an opportunist way. |
| * If this method is updated, please update {@code CodeListUID(CodeList)} accordingly. |
| * |
| * @author Martin Desruisseaux (Geomatys) |
| * @version 0.3 |
| * @since 0.3 |
| * @module |
| */ |
| private static final class CodeTitle extends Description { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = 3306532357801489365L; |
| |
| /** |
| * The code list for which to create a title. |
| */ |
| final CodeList<?> code; |
| |
| /** |
| * Creates a new international string for the given code list element. |
| * |
| * @param code the code list for which to create a title. |
| */ |
| CodeTitle(final CodeList<?> code) { |
| super(CodeLists.RESOURCES, resourceKey(code)); |
| this.code = code; |
| } |
| |
| /** |
| * Returns a fallback if no resource is found. |
| */ |
| @Override |
| String fallback() { |
| return getCodeLabel(code); |
| } |
| } |
| |
| /** |
| * Returns the resource name for the given GeoAPI type, or {@code null} if none. |
| * |
| * @param classname the fully qualified name of the GeoAPI type. |
| * @return the resource bundle to load, or {@code null} if none. |
| */ |
| static String getResources(final String classname) { |
| String resources = "org.opengis.metadata.Descriptions"; |
| if (classname.regionMatches(0, resources, 0, 21)) { // 21 is the location after the last dot. |
| return resources; |
| } |
| // Add more checks here (maybe in a loop) if there is more resource candidates. |
| return null; |
| } |
| |
| /** |
| * Returns all known values for the given type of code list. |
| * Note that the size of the returned array may growth between different invocations of this method, |
| * since users can add their own codes to an existing list. |
| * |
| * @param <T> the compile-time type given as the {@code codeType} parameter. |
| * @param codeType the type of code list. |
| * @return the list of values for the given code list, or an empty array if none. |
| * |
| * @see Class#getEnumConstants() |
| */ |
| public static <T extends CodeList<?>> T[] getCodeValues(final Class<T> codeType) { |
| return CodeLists.values(codeType); |
| } |
| |
| /** |
| * Returns the Java type (usually a GeoAPI interface) for the given ISO name, or {@code null} if none. |
| * The identifier argument shall be the value documented in the {@link UML#identifier()} annotation on |
| * the Java type. |
| * |
| * <div class="note"><b>Examples:</b> |
| * <ul> |
| * <li>{@code forStandardName("CI_Citation")} returns <code>{@linkplain org.opengis.metadata.citation.Citation}.class</code></li> |
| * <li>{@code forStandardName("CS_AxisDirection")} returns <code>{@linkplain org.opengis.referencing.cs.AxisDirection}.class</code></li> |
| * </ul> |
| * </div> |
| * |
| * The package prefix (e.g. {@code "CI_"} in {@code "CI_Citation"}) can be omitted. |
| * The flexibility is provided for allowing transition to newer ISO standards, |
| * which are dropping the package prefixes. |
| * For example {@code "CS_AxisDirection"} in ISO 19111:2007 |
| * has been renamed {@code "AxisDirection"} in ISO 19111:2018. |
| * |
| * <p>Only identifiers for the stable part of GeoAPI or for some Apache SIS classes are recognized. |
| * This method does not handle the identifiers for interfaces in the {@code geoapi-pending} module.</p> |
| * |
| * <div class="note"><b>Future evolution:</b> |
| * when a new ISO type does not yet have a corresponding GeoAPI interface, |
| * this method may temporarily return an Apache SIS class instead, until a future version can use the interface. |
| * For example {@code forStandardName("CI_Individual")} returns |
| * <code>{@linkplain org.apache.sis.metadata.iso.citation.DefaultIndividual}.class</code> in Apache SIS versions |
| * that depend on GeoAPI 3.0, but the return type may be changed to {@code Individual.class} when Apache SIS will |
| * be upgraded to GeoAPI 3.1.</div> |
| * |
| * @param identifier the ISO {@linkplain UML} identifier, or {@code null}. |
| * @return the GeoAPI interface, or {@code null} if the given identifier is {@code null} or unknown. |
| */ |
| public static synchronized Class<?> forStandardName(final String identifier) { |
| if (identifier == null) { |
| return null; |
| } |
| if (typeForNames == null) { |
| final Class<Types> c = Types.class; |
| final InputStream in = c.getResourceAsStream("class-index.properties"); |
| if (in == null) { |
| throw new MissingResourceException("class-index.properties", c.getName(), identifier); |
| } |
| final Properties props = new Properties(); |
| try { |
| props.load(in); |
| in.close(); |
| } catch (IOException | IllegalArgumentException e) { |
| throw new BackingStoreException(e); |
| } |
| /* |
| * Copy all map entries from Properties to a new HashMap for avoiding Properties synchronization. |
| * Also use internized strings because those Strings are programmatic names or annotation values |
| * which are expected to be internized anyway when the corresponding classes are loaded. |
| */ |
| typeForNames = new HashMap<>(Containers.hashMapCapacity(2 * props.size())); |
| for (final Map.Entry<Object,Object> e : props.entrySet()) { |
| final String key = ((String) e.getKey()).intern(); |
| final String value = ((String) e.getValue()).intern(); |
| typeForNames.put(key, value); |
| |
| // Heuristic rule for omitting the prefix (e.g. "CI_" in "CI_Citation"). |
| if (key.length() > 3 && key.charAt(2) == '_' && Character.isUpperCase(key.charAt(1))) { |
| typeForNames.putIfAbsent(key.substring(3).intern(), value); |
| } |
| } |
| // Following code list is not defined in ISO 19115-2 but appears in XML schemas. |
| typeForNames.putIfAbsent("MI_SensorTypeCode", "org.apache.sis.internal.metadata.SensorType"); |
| } |
| /* |
| * Get the interface class for the given identifier, loading the class when first needed. |
| */ |
| final Object value = typeForNames.get(identifier); |
| if (value == null || value instanceof Class<?>) { |
| return (Class<?>) value; |
| } |
| final Class<?> type; |
| try { |
| type = Class.forName((String) value); |
| } catch (ClassNotFoundException e) { |
| throw new TypeNotPresentException((String) value, e); |
| } |
| typeForNames.put(identifier.intern(), type); |
| return type; |
| } |
| |
| /** |
| * Returns the enumeration value of the given type that matches the given name, or {@code null} if none. |
| * This method is similar to the standard {@code Enum.valueOf(…)} method, except that this method is more |
| * tolerant on string comparisons: |
| * |
| * <ul> |
| * <li>Name comparisons are case-insensitive.</li> |
| * <li>Only {@linkplain Character#isLetterOrDigit(int) letter and digit} characters are compared. |
| * Spaces and punctuation characters like {@code '_'} and {@code '-'} are ignored.</li> |
| * </ul> |
| * |
| * If there is no match, this method returns {@code null} — it does not thrown an exception, |
| * unless the given class is not an enumeration. |
| * |
| * @param <T> the compile-time type given as the {@code enumType} parameter. |
| * @param enumType the type of enumeration. |
| * @param name the name of the enumeration value to obtain, or {@code null}. |
| * @return a value matching the given name, or {@code null} if the name is null |
| * or if no matching enumeration is found. |
| * |
| * @see Enum#valueOf(Class, String) |
| * |
| * @since 0.5 |
| */ |
| public static <T extends Enum<T>> T forEnumName(final Class<T> enumType, String name) { |
| return CodeLists.forName(enumType, name); |
| } |
| |
| /** |
| * Returns the code of the given type that matches the given name, or optionally returns a new one if none |
| * match the name. This method performs the same work than the GeoAPI {@code CodeList.valueOf(…)} method, |
| * except that this method is more tolerant on string comparisons when looking for an existing code: |
| * |
| * <ul> |
| * <li>Name comparisons are case-insensitive.</li> |
| * <li>Only {@linkplain Character#isLetterOrDigit(int) letter and digit} characters are compared. |
| * Spaces and punctuation characters like {@code '_'} and {@code '-'} are ignored.</li> |
| * </ul> |
| * |
| * If no match is found, then a new code is created only if the {@code canCreate} argument is {@code true}. |
| * Otherwise this method returns {@code null}. |
| * |
| * @param <T> the compile-time type given as the {@code codeType} parameter. |
| * @param codeType the type of code list. |
| * @param name the name of the code to obtain, or {@code null}. |
| * @param canCreate {@code true} if this method is allowed to create new code. |
| * @return a code matching the given name, or {@code null} if the name is null |
| * or if no matching code is found and {@code canCreate} is {@code false}. |
| * |
| * @see #getCodeName(CodeList) |
| * @see CodeList#valueOf(Class, String) |
| */ |
| public static <T extends CodeList<T>> T forCodeName(final Class<T> codeType, String name, final boolean canCreate) { |
| return CodeLists.forName(codeType, name, canCreate); |
| } |
| |
| /** |
| * Returns the code list or enumeration value for the given title, or {@code null} if none. |
| * The current implementation performs the following choice: |
| * |
| * <ul> |
| * <li>If the given title is a value returned by a previous call to {@link #getCodeTitle(CodeList)}, |
| * returns the code or enumeration value used for creating that title.</li> |
| * <li>Otherwise returns {@code null}.</li> |
| * </ul> |
| * |
| * @param title the title for which to get a code or enumeration value, or {@code null}. |
| * @return the code or enumeration value associated with the given title, or {@code null}. |
| * |
| * @see #getCodeTitle(CodeList) |
| * |
| * @since 0.7 |
| */ |
| public static CodeList<?> forCodeTitle(final CharSequence title) { |
| return (title instanceof CodeTitle) ? ((CodeTitle) title).code : null; |
| } |
| |
| /** |
| * Returns an international string for the values in the given properties map, or {@code null} if none. |
| * This method is used when a property in a {@link java.util.Map} may have many localized variants. |
| * For example the given map may contains a {@code "remarks"} property defined by values associated to |
| * the {@code "remarks_en"} and {@code "remarks_fr"} keys, for English and French locales respectively. |
| * |
| * <p>If the given map is {@code null}, then this method returns {@code null}. |
| * Otherwise this method iterates over the entries having a key that starts with the specified prefix, |
| * followed by the {@code '_'} character. For each such key:</p> |
| * |
| * <ul> |
| * <li>If the key is exactly equals to {@code prefix}, selects {@link Locale#ROOT}.</li> |
| * <li>Otherwise the characters after {@code '_'} are parsed as an ISO language and country code |
| * by the {@link Locales#parse(String, int)} method. Note that 3-letters codes are replaced |
| * by their 2-letters counterparts on a <cite>best effort</cite> basis.</li> |
| * <li>The value for the decoded locale is added in the international string to be returned.</li> |
| * </ul> |
| * |
| * @param properties the map from which to get the string values for an international string, or {@code null}. |
| * @param prefix the prefix of keys to use for creating the international string. |
| * @return the international string, or {@code null} if the given map is null or does not contain values |
| * associated to keys starting with the given prefix. |
| * @throws IllegalArgumentException if a key starts by the given prefix and: |
| * <ul> |
| * <li>The key suffix is an illegal {@link Locale} code,</li> |
| * <li>or the value associated to that key is a not a {@link CharSequence}.</li> |
| * </ul> |
| * |
| * @see Locales#parse(String, int) |
| * @see DefaultInternationalString#DefaultInternationalString(Map) |
| * |
| * @since 0.4 |
| */ |
| public static InternationalString toInternationalString(Map<String,?> properties, final String prefix) |
| throws IllegalArgumentException |
| { |
| ArgumentChecks.ensureNonEmpty("prefix", prefix); |
| if (properties == null) { |
| return null; |
| } |
| /* |
| * If the given map is an instance of SortedMap using the natural ordering of keys, |
| * we can skip all keys that lexicographically precedes the given prefix. |
| */ |
| boolean isSorted = false; |
| if (properties instanceof SortedMap<?,?>) { |
| final SortedMap<String,?> sorted = (SortedMap<String,?>) properties; |
| if (sorted.comparator() == null) { // We want natural ordering. |
| properties = sorted.tailMap(prefix); |
| isSorted = true; |
| } |
| } |
| /* |
| * Now iterates over the map entry and lazily create the InternationalString |
| * only when first needed. In most cases, we have 0 or 1 matching entry. |
| */ |
| CharSequence i18n = null; |
| Locale firstLocale = null; |
| DefaultInternationalString dis = null; |
| final int offset = prefix.length(); |
| for (final Map.Entry<String,?> entry : properties.entrySet()) { |
| final String key = entry.getKey(); |
| if (key == null) { |
| continue; // Tolerance for Map that accept null keys. |
| } |
| if (!key.startsWith(prefix)) { |
| if (isSorted) break; // If the map is sorted, there is no need to check next entries. |
| continue; |
| } |
| final Locale locale; |
| if (key.length() == offset) { |
| locale = Locale.ROOT; |
| } else { |
| final char c = key.charAt(offset); |
| if (c != '_') { |
| if (isSorted && c > '_') break; |
| continue; |
| } |
| final int s = offset + 1; |
| try { |
| locale = Locales.parse(key, s); |
| } catch (IllformedLocaleException e) { |
| throw new IllegalArgumentException(Errors.getResources(properties).getString( |
| Errors.Keys.IllegalLanguageCode_1, '(' + key.substring(0, s) + ')' + key.substring(s), e)); |
| } |
| } |
| final Object value = entry.getValue(); |
| if (value != null) { |
| if (!(value instanceof CharSequence)) { |
| throw new IllegalArgumentException(Errors.getResources(properties) |
| .getString(Errors.Keys.IllegalPropertyValueClass_2, key, value.getClass())); |
| } |
| if (i18n == null) { |
| i18n = (CharSequence) value; |
| firstLocale = locale; |
| } else { |
| if (dis == null) { |
| dis = new DefaultInternationalString(); |
| dis.add(firstLocale, i18n); |
| i18n = dis; |
| } |
| dis.add(locale, (CharSequence) value); |
| } |
| } |
| } |
| return toInternationalString(i18n); |
| } |
| |
| /** |
| * Returns the given characters sequence as an international string. If the given sequence is |
| * null or an instance of {@link InternationalString}, then this method returns it unchanged. |
| * Otherwise, this method copies the {@link InternationalString#toString()} value in a new |
| * {@link SimpleInternationalString} instance and returns it. |
| * |
| * @param string the characters sequence to convert, or {@code null}. |
| * @return the given sequence as an international string, or {@code null} if the given sequence was null. |
| * |
| * @see DefaultNameFactory#createInternationalString(Map) |
| */ |
| public static InternationalString toInternationalString(final CharSequence string) { |
| if (string == null || string instanceof InternationalString) { |
| return (InternationalString) string; |
| } |
| return new SimpleInternationalString(string.toString()); |
| } |
| |
| /** |
| * Returns the given array of {@code CharSequence}s as an array of {@code InternationalString}s. |
| * If the given array is null or an instance of {@code InternationalString[]}, then this method |
| * returns it unchanged. Otherwise a new array of type {@code InternationalString[]} is created |
| * and every elements from the given array is copied or |
| * {@linkplain #toInternationalString(CharSequence) casted} in the new array. |
| * |
| * <p>If a defensive copy of the {@code strings} array is wanted, then the caller needs to check |
| * if the returned array is the same instance than the one given in argument to this method.</p> |
| * |
| * @param strings the characters sequences to convert, or {@code null}. |
| * @return the given array as an array of type {@code InternationalString[]}, |
| * or {@code null} if the given array was null. |
| */ |
| public static InternationalString[] toInternationalStrings(final CharSequence... strings) { |
| if (strings == null || strings instanceof InternationalString[]) { |
| return (InternationalString[]) strings; |
| } |
| final InternationalString[] copy = new InternationalString[strings.length]; |
| for (int i=0; i<strings.length; i++) { |
| copy[i] = toInternationalString(strings[i]); |
| } |
| return copy; |
| } |
| |
| /** |
| * Returns the given international string in the given locale, or {@code null} if the given string is null. |
| * If the given locale is {@code null}, then the {@code i18n} default locale is used. |
| * |
| * @param i18n the international string to get as a localized string, or {@code null} if none. |
| * @param locale the desired locale, or {@code null} for the {@code i18n} default locale. |
| * @return the localized string, or {@code null} if {@code i18n} is {@code null}. |
| * |
| * @since 0.8 |
| */ |
| public static String toString(final InternationalString i18n, final Locale locale) { |
| return (i18n == null) ? null : (locale == null) ? i18n.toString() : i18n.toString(locale); |
| } |
| } |