/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-2008, Open Source Geospatial Foundation (OSGeo) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. */ package org.geotools.util; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Set; import org.opengis.util.InternationalString; import org.geotools.util.logging.Logging; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * An implementation of international string using a {@linkplain Map map} * of strings for different {@linkplain Locale locales}. Strings for new * locales can be {@linkplain #add(Locale,String) added}, but existing * strings can't be removed or modified. This behavior is a compromise * between making constructionss easier, and being suitable for use in * immutable objects. * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class GrowableInternationalString extends AbstractInternationalString implements Serializable { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = 5760033376627376937L; /** * The set of locales created in this virtual machine through methods of this class. * Used in order to get a {@linkplain #unique unique} instance of {@link Locale} objects. */ private static final Map<Locale,Locale> LOCALES = new HashMap<Locale,Locale>(); /** * The string values in different locales (never {@code null}). * Keys are {@link Locale} objects and values are {@link String}s. */ private Map<Locale,String> localMap; /** * An unmodifiable view of the entry set in {@link #localMap}. This is the set of locales * defined in this international string. Will be constructed only when first requested. */ private transient Set<Locale> localSet; /** * Constructs an initially empty international string. Localized strings can been added * using one of {@link #add add(...)} methods. */ public GrowableInternationalString() { localMap = Collections.emptyMap(); } /** * Constructs an international string initialized with the specified string. * Additional localized strings can been added using one of {@link #add add(...)} * methods. The string specified to this constructor is the one that will be * returned if no localized string is found for the {@link Locale} argument * in a call to {@link #toString(Locale)}. * * @param string The string in no specific locale. */ public GrowableInternationalString(final String string) { if (string != null) { localMap = Collections.singletonMap(null, string); } else { localMap = Collections.emptyMap(); } } /** * Adds a string for the given locale. * * @param locale The locale for the {@code string} value, or {@code null}. * @param string The localized string. * @throws IllegalArgumentException if a different string value was already set for * the given locale. */ public synchronized void add(final Locale locale, final String string) throws IllegalArgumentException { if (string != null) { switch (localMap.size()) { case 0: { localMap = Collections.singletonMap(locale, string); defaultValue = null; // Will be recomputed when first needed. return; } case 1: { localMap = new HashMap<Locale,String>(localMap); break; } } String old = localMap.get(locale); if (old != null) { if (string.equals(old)) { return; } // TODO: provide a localized message "String value already set for locale ...". throw new IllegalArgumentException(); } localMap.put(locale, string); defaultValue = null; // Will be recomputed when first needed. } } /** * Adds a string for the given property key. This is a convenience method for constructing an * {@code AbstractInternationalString} during iteration through the * {@linkplain java.util.Map.Entry entries} in a {@link Map}. It infers the {@link Locale} * from the property {@code key}, using the following steps: * <ul> * <li>If the {@code key} do not starts with the specified {@code prefix}, then * this method do nothing and returns {@code false}.</li> * <li>Otherwise, the characters after the {@code prefix} are parsed as an ISO language * and country code, and the {@link #add(Locale,String)} method is * invoked.</li> * </ul> * * <P>For example if the prefix is <code>"remarks"</code>, then the <code>"remarks_fr"</code> * property key stands for remarks in {@linkplain Locale#FRENCH French} while the * <code>"remarks_fr_CA"</code> property key stands for remarks in * {@linkplain Locale#CANADA_FRENCH French Canadian}.</P> * * @param prefix The prefix to skip at the begining of the {@code key}. * @param key The property key. * @param string The localized string for the specified {@code key}. * @return {@code true} if the key has been recognized, or {@code false} otherwise. * @throws IllegalArgumentException if the locale after the prefix is an illegal code, or a * different string value was already set for the given locale. */ public boolean add(final String prefix, final String key, final String string) throws IllegalArgumentException { if (!key.startsWith(prefix)) { return false; } int position = prefix.length(); final int length = key.length(); final String[] parts = new String[] {"", "", ""}; for (int i=0; /*break condition inside*/; i++) { if (position == length) { final Locale locale = (i==0) ? (Locale)null : unique(new Locale(parts[0] /* language */, parts[1] /* country */, parts[2] /* variant */)); add(locale, string); return true; } if (key.charAt(position)!='_' || i==parts.length) { // Unknow character, or two many characters break; } int next = key.indexOf('_', ++position); if (next < 0) { next = length; } else if (next == position) { // Found two consecutive '_' characters break; } parts[i] = key.substring(position, position=next); } throw new IllegalArgumentException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2, "locale", key.substring(prefix.length()))); } /** * Returns a canonical instance of the given locale. * * @param locale The locale to canonicalize. * @return The canonical instance of {@code locale}. */ private static synchronized Locale unique(final Locale locale) { /** * Initialize the LOCALES map with the set of locales defined in the Locale class. * This operation is done only once. */ if (LOCALES.isEmpty()) try { final Field[] fields = Locale.class.getFields(); for (int i=0; i<fields.length; i++) { final Field field = fields[i]; if (Modifier.isStatic(field.getModifiers())) { if (Locale.class.isAssignableFrom(field.getType())) { final Locale toAdd = (Locale) field.get(null); LOCALES.put(toAdd, toAdd); } } } } catch (Exception exception) { /* * Not a big deal if this operation fails (this is actually just an * optimization for reducing memory usage). Log a warning and continue. */ Logging.unexpectedException(GrowableInternationalString.class, "unique", exception); } /* * Now canonicalize the locale. */ final Locale candidate = LOCALES.get(locale); if (candidate != null) { return candidate; } LOCALES.put(locale, locale); return locale; } /** * Returns the set of locales defined in this international string. * * @return The set of locales. */ public synchronized Set<Locale> getLocales() { if (localSet == null) { localSet = Collections.unmodifiableSet(localMap.keySet()); } return localSet; } /** * Returns a string in the specified locale. If there is no string for the specified * {@code locale}, then this method search for a locale without the * {@linkplain Locale#getVariant variant} part. If no string are found, * then this method search for a locale without the {@linkplain Locale#getCountry country} * part. For example if the <code>"fr_CA"</code> locale was requested but not found, then * this method looks for the <code>"fr"</code> locale. The {@code null} locale * (which stand for unlocalized message) is tried last. * * @param locale The locale to look for, or {@code null}. * @return The string in the specified locale, or in a default locale. */ public synchronized String toString(Locale locale) { String text; while (locale != null) { text = localMap.get(locale); if (text != null) { return text; } final String language = locale.getLanguage(); final String country = locale.getCountry (); final String variant = locale.getVariant (); if (variant.length() != 0) { locale = new Locale(language, country); continue; } if (country.length() != 0) { locale = new Locale(language); continue; } break; } // Tries the string in the 'null' locale. text = localMap.get(null); if (text == null) { // No 'null' locale neither. Returns the first string in whatever locale. final Iterator<String> it = localMap.values().iterator(); if (it.hasNext()) { return it.next(); } } return text; } /** * Returns {@code true} if all localized texts stored in this international string are * contained in the specified object. More specifically: * * <ul> * <li><p>If {@code candidate} is an instance of {@link InternationalString}, then this method * returns {@code true} if, for all <var>{@linkplain Locale locale}</var>-<var>{@linkplain * String string}</var> pairs contained in {@code this}, <code>candidate.{@linkplain * InternationalString#toString(Locale) toString}(locale)</code> returns a string * {@linkplain String#equals equals} to {@code string}.</p></li> * * <li><p>If {@code candidate} is an instance of {@link CharSequence}, then this method * returns {@code true} if {@link #toString(Locale)} returns a string {@linkplain * String#equals equals} to <code>candidate.{@linkplain CharSequence#toString() * toString()}</code> for all locales.</p></li> * * <li><p>If {@code candidate} is an instance of {@link Map}, then this methods returns * {@code true} if all <var>{@linkplain Locale locale}</var>-<var>{@linkplain String * string}</var> pairs are contained into {@code candidate}.</p></li> * * <li><p>Otherwise, this method returns {@code false}.</p></li> * </ul> * * @param candidate The object which may contains this international string. * @return {@code true} if the given object contains all localized strings found in this * international string. * * @since 2.3 */ public boolean isSubsetOf(final Object candidate) { if (candidate instanceof InternationalString) { final InternationalString string = (InternationalString) candidate; for (final Map.Entry<Locale,String> entry : localMap.entrySet()) { final Locale locale = entry.getKey(); final String text = entry.getValue(); if (!text.equals(string.toString(locale))) { return false; } } } else if (candidate instanceof CharSequence) { final String string = candidate.toString(); for (final String text : localMap.values()) { if (!text.equals(string)) { return false; } } } else if (candidate instanceof Map) { final Map<?,?> map = (Map<?,?>) candidate; return map.entrySet().containsAll(localMap.entrySet()); } else { return false; } return true; } /** * Compares this international string with the specified object for equality. * * @param object The object to compare with this international string. * @return {@code true} if the given object is equals to this string. */ @Override public boolean equals(final Object object) { if (object!=null && object.getClass().equals(getClass())) { final GrowableInternationalString that = (GrowableInternationalString) object; return Utilities.equals(this.localMap, that.localMap); } return false; } /** * Returns a hash code value for this international text. */ @Override public int hashCode() { return (int)serialVersionUID ^ localMap.hashCode(); } /** * Canonicalize the locales after deserialization. */ private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); final int size = localMap.size(); if (size == 0) { return; } @SuppressWarnings("unchecked") Map.Entry<Locale,String>[] entries = new Map.Entry[size]; entries = localMap.entrySet().toArray(entries); if (size == 1) { final Map.Entry<Locale,String> entry = entries[0]; localMap = Collections.singletonMap(unique(entry.getKey()), entry.getValue()); } else { localMap.clear(); for (int i=0; i<entries.length; i++) { final Map.Entry<Locale,String> entry = entries[i]; localMap.put(unique(entry.getKey()), entry.getValue()); } } } }