| /* |
| * 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 java.lang.reflect.Array; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.UndeclaredThrowableException; |
| import org.opengis.annotation.UML; |
| import org.opengis.util.CodeList; |
| import org.opengis.util.InternationalString; |
| import org.opengis.util.ControlledVocabulary; |
| 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.BackingStoreException; |
| import org.apache.sis.internal.system.Loggers; |
| |
| // Branch-dependent imports |
| import org.apache.sis.internal.jdk8.JDK8; |
| |
| |
| /** |
| * 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(ControlledVocabulary)} 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(ControlledVocabulary)} for ISO name,</li> |
| * <li>{@link #getCodeTitle(ControlledVocabulary)} for a label or title</li> |
| * <li>{@link #getDescription(ControlledVocabulary)} 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 <gmi:type>} value is normally a {@code <gco:CharacterString>} |
| * but has been replaced by a {@code SensorType} code below: |
| * |
| * {@preformat xml |
| * <gmi:MI_Instrument> |
| * <gmi:type> |
| * <gmi:MI_SensorTypeCode |
| * codeList="http://navigator.eumetsat.int/metadata_schema/eum/resources/Codelist/eum_gmxCodelists.xml#CI_SensorTypeCode" |
| * codeListValue="RADIOMETER">Radiometer</gmi:MI_SensorTypeCode> |
| * </gmi:type> |
| * </gmi:MI_Instrument> |
| * } |
| * </div> |
| * |
| * Such substitution can be done with: |
| * |
| * <ul> |
| * <li>{@link #getCodeTitle(ControlledVocabulary)} 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) |
| * @since 0.3 |
| * @version 0.7 |
| * @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<Object,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(CharacterSet.UTF_8)} returns {@code "MD_CharacterSetCode"}.</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 ControlledVocabulary code) { |
| if (code == null) { |
| return null; |
| } |
| final Class<?> type = (code instanceof Enum<?>) ? ((Enum<?>) code).getDeclaringClass() : 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(CharacterSet.UTF_8)} returns {@code "utf8"}.</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(ControlledVocabulary) |
| * @see #getCodeTitle(ControlledVocabulary) |
| * @see #getDescription(ControlledVocabulary) |
| * @see #forCodeName(Class, String, boolean) |
| */ |
| public static String getCodeName(final ControlledVocabulary 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(ControlledVocabulary)} 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(CharacterSet.UTF_8)} returns {@code "UTF-8"}.</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(ControlledVocabulary) |
| * @see #getCodeTitle(ControlledVocabulary) |
| * @see #getDescription(ControlledVocabulary) |
| */ |
| public static String getCodeLabel(final ControlledVocabulary 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(ControlledVocabulary) 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(ControlledVocabulary) |
| * @see #forCodeTitle(CharSequence) |
| */ |
| public static InternationalString getCodeTitle(final ControlledVocabulary 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(ControlledVocabulary) |
| * @see #getDescription(Class) |
| */ |
| public static InternationalString getDescription(final ControlledVocabulary 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(ControlledVocabulary) |
| */ |
| 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) |
| * @since 0.3 |
| * @version 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.recoverableException(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 ControlledVocabulary 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) |
| * @since 0.3 |
| * @version 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 ControlledVocabulary 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 ControlledVocabulary code) { |
| super("org.opengis.metadata.CodeLists", 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 or enumeration. |
| * 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. |
| * |
| * <div class="note"><b>Note:</b> |
| * This method works with both {@link Enum} and {@link CodeList}. However if the type is known to be an |
| * {@code Enum}, then the standard {@link Class#getEnumConstants()} method is more efficient.</div> |
| * |
| * @param <T> The compile-time type given as the {@code codeType} parameter. |
| * @param codeType The type of code list or enumeration. |
| * @return The list of values for the given code list or enumeration, or an empty array if none. |
| * |
| * @see Class#getEnumConstants() |
| */ |
| @SuppressWarnings("unchecked") |
| public static <T extends ControlledVocabulary> T[] getCodeValues(final Class<T> codeType) { |
| Object values; |
| try { |
| values = codeType.getMethod("values", (Class<?>[]) null).invoke(null, (Object[]) null); |
| } catch (InvocationTargetException e) { |
| final Throwable cause = e.getCause(); |
| if (cause instanceof RuntimeException) { |
| throw (RuntimeException) cause; |
| } |
| if (cause instanceof Error) { |
| throw (Error) cause; |
| } |
| throw new UndeclaredThrowableException(cause); |
| } catch (NoSuchMethodException | IllegalAccessException e) { |
| values = Array.newInstance(codeType, 0); |
| } |
| return (T[]) values; |
| } |
| |
| /** |
| * Returns the 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 associated with the GeoAPI interface. |
| * |
| * <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> |
| * |
| * Only identifiers for the stable part of GeoAPI are recognized. This method does not handle |
| * the identifiers for the {@code geoapi-pending} module. |
| * |
| * @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<UML> c = UML.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); |
| } |
| typeForNames = new HashMap<>(props); |
| JDK8.putIfAbsent(typeForNames, "MI_SensorTypeCode", "org.apache.sis.internal.metadata.SensorType"); |
| } |
| 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, 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) { |
| name = CharSequences.trimWhitespaces(name); |
| if (name != null && !name.isEmpty()) try { |
| return Enum.valueOf(enumType, name); |
| } catch (IllegalArgumentException e) { |
| final T[] values = enumType.getEnumConstants(); |
| if (values == null) { |
| throw e; |
| } |
| if (values instanceof ControlledVocabulary[]) { |
| for (final ControlledVocabulary code : (ControlledVocabulary[]) values) { |
| for (final String candidate : code.names()) { |
| if (CodeListFilter.accept(candidate, name)) { |
| return enumType.cast(code); |
| } |
| } |
| } |
| } else { |
| for (final Enum<?> code : values) { |
| if (CodeListFilter.accept(code.name(), name)) { |
| return enumType.cast(code); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * 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(ControlledVocabulary) |
| * @see CodeList#valueOf(Class, String) |
| */ |
| public static <T extends CodeList<T>> T forCodeName(final Class<T> codeType, String name, final boolean canCreate) { |
| name = CharSequences.trimWhitespaces(name); |
| if (name == null || name.isEmpty()) { |
| return null; |
| } |
| return CodeList.valueOf(codeType, new CodeListFilter(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(ControlledVocabulary)}, |
| * 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}. |
| * |
| * @since 0.7 |
| * |
| * @see #getCodeTitle(ControlledVocabulary) |
| */ |
| public static ControlledVocabulary 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; |
| } |
| } |