001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2004-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2004, Institut de Recherche pour le D�veloppement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.util;
018:
019: // J2SE dependencies
020: import java.io.IOException;
021: import java.io.ObjectInputStream;
022: import java.io.Serializable;
023: import java.lang.reflect.Field;
024: import java.lang.reflect.Modifier;
025: import java.util.Collections;
026: import java.util.HashMap;
027: import java.util.Iterator;
028: import java.util.Locale;
029: import java.util.Map;
030: import java.util.Set;
031:
032: // OpenGIS utilities
033: import org.opengis.util.InternationalString;
034: import org.geotools.util.logging.Logging;
035: import org.geotools.resources.Utilities;
036: import org.geotools.resources.i18n.Errors;
037: import org.geotools.resources.i18n.ErrorKeys;
038:
039: /**
040: * An implementation of international string using a {@linkplain Map map}
041: * of strings for different {@linkplain Locale locales}. Strings for new
042: * locales can be {@linkplain #add(Locale,String) added}, but existing
043: * strings can't be removed or modified. This behavior is a compromise
044: * between making constructionss easier, and being suitable for use in
045: * immutable objects.
046: *
047: * @since 2.1
048: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/util/GrowableInternationalString.java $
049: * @version $Id: GrowableInternationalString.java 27848 2007-11-12 13:10:32Z desruisseaux $
050: * @author Martin Desruisseaux
051: */
052: public class GrowableInternationalString extends
053: AbstractInternationalString implements Serializable {
054: /**
055: * Serial number for interoperability with different versions.
056: */
057: private static final long serialVersionUID = 5760033376627376937L;
058:
059: /**
060: * The set of locales created in this virtual machine through methods of this class.
061: * Used in order to get a {@linkplain #unique unique} instance of {@link Locale} objects.
062: */
063: private static final Map LOCALES = new HashMap();
064:
065: /**
066: * The string values in different locales (never {@code null}).
067: * Keys are {@link Locale} objects and values are {@link String}s.
068: */
069: private Map localMap;
070:
071: /**
072: * An unmodifiable view of the entry set in {@link #localMap}. This is the set of locales
073: * defined in this international string. Will be constructed only when first requested.
074: */
075: private transient Set localSet;
076:
077: /**
078: * Constructs an initially empty international string. Localized strings can been added
079: * using one of {@link #add add(...)} methods.
080: */
081: public GrowableInternationalString() {
082: localMap = Collections.EMPTY_MAP;
083: }
084:
085: /**
086: * Constructs an international string initialized with the specified string.
087: * Additional localized strings can been added using one of {@link #add add(...)}
088: * methods. The string specified to this constructor is the one that will be
089: * returned if no localized string is found for the {@link Locale} argument
090: * in a call to {@link #toString(Locale)}.
091: *
092: * @param string The string in no specific locale.
093: */
094: public GrowableInternationalString(final String string) {
095: if (string != null) {
096: localMap = Collections.singletonMap(null, string);
097: } else {
098: localMap = Collections.EMPTY_MAP;
099: }
100: }
101:
102: /**
103: * Add a string for the given locale.
104: *
105: * @param locale The locale for the {@code string} value, or {@code null}.
106: * @param string The localized string.
107: * @throws IllegalArgumentException if a different string value was already set for
108: * the given locale.
109: */
110: public synchronized void add(final Locale locale,
111: final String string) throws IllegalArgumentException {
112: if (string != null) {
113: switch (localMap.size()) {
114: case 0: {
115: localMap = Collections.singletonMap(locale, string);
116: defaultValue = null; // Will be recomputed when first needed.
117: return;
118: }
119: case 1: {
120: localMap = new HashMap(localMap);
121: break;
122: }
123: }
124: String old = (String) localMap.get(locale);
125: if (old != null) {
126: if (string.equals(old)) {
127: return;
128: }
129: // TODO: provide a localized message "String value already set for locale ...".
130: throw new IllegalArgumentException();
131: }
132: localMap.put(locale, string);
133: defaultValue = null; // Will be recomputed when first needed.
134: }
135: }
136:
137: /**
138: * Add a string for the given property key. This is a convenience method for constructing an
139: * {@code AbstractInternationalString} during iteration through the
140: * {@linkplain java.util.Map.Entry entries} in a {@link Map}. It infers the {@link Locale}
141: * from the property {@code key}, using the following steps:
142: * <ul>
143: * <li>If the {@code key} do not starts with the specified {@code prefix}, then
144: * this method do nothing and returns {@code false}.</li>
145: * <li>Otherwise, the characters after the {@code prefix} are parsed as an ISO language
146: * and country code, and the {@link #add(Locale,String)} method is
147: * invoked.</li>
148: * </ul>
149: *
150: * <P>For example if the prefix is <code>"remarks"</code>, then the <code>"remarks_fr"</code>
151: * property key stands for remarks in {@linkplain Locale#FRENCH French} while the
152: * <code>"remarks_fr_CA"</code> property key stands for remarks in
153: * {@linkplain Locale#CANADA_FRENCH French Canadian}.</P>
154: *
155: * @param prefix The prefix to skip at the begining of the {@code key}.
156: * @param key The property key.
157: * @param string The localized string for the specified {@code key}.
158: * @return {@code true} if the key has been recognized, or {@code false} otherwise.
159: * @throws IllegalArgumentException if the locale after the prefix is an illegal code, or a
160: * different string value was already set for the given locale.
161: */
162: public boolean add(final String prefix, final String key,
163: final String string) throws IllegalArgumentException {
164: if (!key.startsWith(prefix)) {
165: return false;
166: }
167: int position = prefix.length();
168: final int length = key.length();
169: final String[] parts = new String[] { "", "", "" };
170: for (int i = 0; /*break condition inside*/; i++) {
171: if (position == length) {
172: final Locale locale = (i == 0) ? (Locale) null
173: : unique(new Locale(parts[0] /* language */,
174: parts[1] /* country */, parts[2] /* variant */));
175: add(locale, string);
176: return true;
177: }
178: if (key.charAt(position) != '_' || i == parts.length) {
179: // Unknow character, or two many characters
180: break;
181: }
182: int next = key.indexOf('_', ++position);
183: if (next < 0) {
184: next = length;
185: } else if (next == position) {
186: // Found two consecutive '_' characters
187: break;
188: }
189: parts[i] = key.substring(position, position = next);
190: }
191: throw new IllegalArgumentException(Errors.format(
192: ErrorKeys.ILLEGAL_ARGUMENT_$2, "locale", key
193: .substring(prefix.length())));
194: }
195:
196: /**
197: * Returns a canonical instance of the given locale.
198: *
199: * @param locale The locale to canonicalize.
200: * @return The canonical instance of {@code locale}.
201: */
202: private static synchronized Locale unique(final Locale locale) {
203: /**
204: * Initialize the LOCALES map with the set of locales defined in the Locale class.
205: * This operation is done only once.
206: */
207: if (LOCALES.isEmpty())
208: try {
209: final Field[] fields = Locale.class.getFields();
210: for (int i = 0; i < fields.length; i++) {
211: final Field field = fields[i];
212: if (Modifier.isStatic(field.getModifiers())) {
213: if (Locale.class.isAssignableFrom(field
214: .getType())) {
215: final Locale toAdd = (Locale) field
216: .get(null);
217: LOCALES.put(toAdd, toAdd);
218: }
219: }
220: }
221: } catch (Exception exception) {
222: /*
223: * Not a big deal if this operation fails (this is actually just an
224: * optimization for reducing memory usage). Log a warning and continue.
225: */
226: Logging.unexpectedException("org.geotools.util",
227: GrowableInternationalString.class, "unique",
228: exception);
229: }
230: /*
231: * Now canonicalize the locale.
232: */
233: final Locale candidate = (Locale) LOCALES.get(locale);
234: if (candidate != null) {
235: return candidate;
236: }
237: LOCALES.put(locale, locale);
238: return locale;
239: }
240:
241: /**
242: * Returns the set of locales defined in this international string.
243: */
244: public Set getLocales() {
245: // No need to synchronize; this is not a big deal if this object is built twice.
246: if (localSet == null) {
247: localSet = Collections.unmodifiableSet(localMap.entrySet());
248: }
249: return localSet;
250: }
251:
252: /**
253: * Returns a string in the specified locale. If there is no string for the specified
254: * {@code locale}, then this method search for a locale without the
255: * {@linkplain Locale#getVariant variant} part. If no string are found,
256: * then this method search for a locale without the {@linkplain Locale#getCountry country}
257: * part. For example if the <code>"fr_CA"</code> locale was requested but not found, then
258: * this method looks for the <code>"fr"</code> locale. The {@code null} locale
259: * (which stand for unlocalized message) is tried last.
260: *
261: * @param locale The locale to look for, or {@code null}.
262: * @return The string in the specified locale, or in a default locale.
263: */
264: public String toString(Locale locale) {
265: String text;
266: while (locale != null) {
267: text = (String) localMap.get(locale);
268: if (text != null) {
269: return text;
270: }
271: final String language = locale.getLanguage();
272: final String country = locale.getCountry();
273: final String variant = locale.getVariant();
274: if (variant.length() != 0) {
275: locale = new Locale(language, country);
276: continue;
277: }
278: if (country.length() != 0) {
279: locale = new Locale(language);
280: continue;
281: }
282: break;
283: }
284:
285: // Try the string in the 'null' locale.
286: text = (String) localMap.get(null);
287: if (text == null) {
288: // No 'null' locale neither. Returns the first string in whatever locale.
289: final Iterator it = localMap.values().iterator();
290: if (it.hasNext()) {
291: return (String) it.next();
292: }
293: }
294: return text;
295: }
296:
297: /**
298: * Returns {@code true} if all localized texts stored in this international string are
299: * contained in the specified object. More specifically:
300: *
301: * <ul>
302: * <li><p>If {@code candidate} is an instance of {@link InternationalString}, then this method
303: * returns {@code true} if, for all <var>{@linkplain Locale locale}</var>-<var>{@linkplain
304: * String string}</var> pairs contained in {@code this}, <code>candidate.{@linkplain
305: * InternationalString#toString(Locale) toString}(locale)</code> returns a string
306: * {@linkplain String#equals equals} to {@code string}.</p></li>
307: *
308: * <li><p>If {@code candidate} is an instance of {@link CharSequence}, then this method
309: * returns {@code true} if {@link #toString(Locale)} returns a string {@linkplain
310: * String#equals equals} to <code>candidate.{@linkplain CharSequence#toString()
311: * toString()}</code> for all locales.</p></li>
312: *
313: * <li><p>If {@code candidate} is an instance of {@link Map}, then this methods returns
314: * {@code true} if all <var>{@linkplain Locale locale}</var>-<var>{@linkplain String
315: * string}</var> pairs are contained into {@code candidate}.</p></li>
316: *
317: * <li><p>Otherwise, this method returns {@code false}.</p></li>
318: * </ul>
319: *
320: * @since 2.3
321: */
322: public boolean isSubsetOf(final Object candidate) {
323: if (candidate instanceof InternationalString) {
324: final InternationalString string = (InternationalString) candidate;
325: for (final Iterator it = localMap.entrySet().iterator(); it
326: .hasNext();) {
327: final Map.Entry entry = (Map.Entry) it.next();
328: final Locale locale = (Locale) entry.getKey();
329: final String text = (String) entry.getValue();
330: if (!text.equals(string.toString(locale))) {
331: return false;
332: }
333: }
334: } else if (candidate instanceof CharSequence) {
335: final String string = candidate.toString();
336: for (final Iterator it = localMap.values().iterator(); it
337: .hasNext();) {
338: final String text = (String) it.next();
339: if (!text.equals(string)) {
340: return false;
341: }
342: }
343: } else if (candidate instanceof Map) {
344: return ((Map) candidate).entrySet().containsAll(
345: localMap.entrySet());
346: } else {
347: return false;
348: }
349: return true;
350: }
351:
352: /**
353: * Compares this international string with the specified object for equality.
354: */
355: public boolean equals(final Object object) {
356: if (object != null && object.getClass().equals(getClass())) {
357: final GrowableInternationalString that = (GrowableInternationalString) object;
358: return Utilities.equals(this .localMap, that.localMap);
359: }
360: return false;
361: }
362:
363: /**
364: * Returns a hash code value for this international text.
365: */
366: public int hashCode() {
367: return (int) serialVersionUID ^ localMap.hashCode();
368: }
369:
370: /**
371: * Canonicalize the locales after deserialization.
372: */
373: private void readObject(final ObjectInputStream in)
374: throws IOException, ClassNotFoundException {
375: in.defaultReadObject();
376: final int size = localMap.size();
377: if (size == 0) {
378: return;
379: }
380: final Map.Entry[] entries;
381: entries = (Map.Entry[]) localMap.entrySet().toArray(
382: new Map.Entry[size]);
383: if (size == 1) {
384: final Map.Entry entry = entries[0];
385: localMap = Collections.singletonMap(unique((Locale) entry
386: .getKey()), entry.getValue());
387: } else {
388: localMap.clear();
389: for (int i = 0; i < entries.length; i++) {
390: final Map.Entry entry = entries[i];
391: localMap.put(unique((Locale) entry.getKey()), entry
392: .getValue());
393: }
394: }
395: }
396: }
|