001: /******************************************************************************
002: * JBoss, a division of Red Hat *
003: * Copyright 2006, Red Hat Middleware, LLC, and individual *
004: * contributors as indicated by the @authors tag. See the *
005: * copyright.txt in the distribution for a full listing of *
006: * individual contributors. *
007: * *
008: * This is free software; you can redistribute it and/or modify it *
009: * under the terms of the GNU Lesser General Public License as *
010: * published by the Free Software Foundation; either version 2.1 of *
011: * the License, or (at your option) any later version. *
012: * *
013: * This software is distributed in the hope that it will be useful, *
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of *
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
016: * Lesser General Public License for more details. *
017: * *
018: * You should have received a copy of the GNU Lesser General Public *
019: * License along with this software; if not, write to the Free *
020: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA *
021: * 02110-1301 USA, or see the FSF site: http://www.fsf.org. *
022: ******************************************************************************/package org.jboss.portal.migration.model24.identity;
023:
024: import org.apache.log4j.Logger;
025:
026: import java.util.ArrayList;
027: import java.util.Arrays;
028: import java.util.Collection;
029: import java.util.Collections;
030: import java.util.Comparator;
031: import java.util.HashMap;
032: import java.util.List;
033: import java.util.Locale;
034: import java.util.Map;
035: import java.util.regex.Matcher;
036: import java.util.regex.Pattern;
037:
038: /**
039: * Additional infos about a locale.
040: *
041: * @author <a href="mailto:julien@jboss.org">Julien Viet</a>
042: * @version $Revision: 8784 $
043: * @noinspection ALL
044: */
045: public final class LocaleInfo {
046:
047: // Static fields ****************************************************************************************************
048:
049: /** The logger. */
050: private static Logger log = Logger.getLogger(LocaleInfo.class);
051:
052: /** The locale info map. */
053: private static volatile Infos sharedInfos = createFromAvailableLocales();
054:
055: /** Separator. */
056: private static final String RFC3066_SEPARATOR = "-";
057:
058: /** Valid language tag matcher (see <a href="http://www.ietf.org/rfc/rfc3066.txt">IETF RFC 3066</a>). */
059: private static final Pattern RFC3066_COMPOUND_LANG_PATTERN = Pattern
060: .compile("(\\p{Lower}{2})(-(\\p{Upper}{2}))?");
061:
062: /** Sorted valid ISO country codes (needed for Arrays.binarySearch). */
063: private static final String[] SORTED_ISO_COUNTRIES = Locale
064: .getISOCountries();
065:
066: /** Sorted valid ISO language codes (needed for Arrays.binarySearch). */
067: private static final String[] SORTED_ISO_LANGUAGES = Locale
068: .getISOLanguages();
069:
070: private static final Comparator c = new Comparator() {
071: public int compare(Object o1, Object o2) {
072: String s1 = ((LocaleInfo) o1).getTrailingName();
073: String s2 = ((LocaleInfo) o2).getTrailingName();
074: return s1.compareTo(s2);
075: }
076: };
077:
078: static {
079: // should already be sorted but in case they're not...
080: Arrays.sort(SORTED_ISO_COUNTRIES);
081: Arrays.sort(SORTED_ISO_LANGUAGES);
082: }
083:
084: // Fields ***********************************************************************************************************
085:
086: /** The parent locale info or null if no parent. */
087: private LocaleInfo parent;
088:
089: /** The relaled locale. */
090: private Locale locale;
091:
092: /** The trailing name used to compute resource bundle name. */
093: private String trailingName;
094:
095: /** The RFC3066 language tag. */
096: private String rfc3066LangageTag;
097:
098: // Constructor ******************************************************************************************************
099:
100: private LocaleInfo(LocaleInfo parent, Locale locale) {
101: this .locale = locale;
102: this .trailingName = computeTrailingName(locale);
103: this .parent = parent;
104:
105: //
106: this .rfc3066LangageTag = getRFC3066LanguageTagFor(locale);
107: }
108:
109: // Public ***********************************************************************************************************
110:
111: public LocaleInfo getParent() {
112: return parent;
113: }
114:
115: public Locale getLocale() {
116: return locale;
117: }
118:
119: public String getTrailingName() {
120: return trailingName;
121: }
122:
123: /**
124: * Retrieves the language identification tag associated to this LocaleInfo as defined by <a
125: * href="http://www.ietf.org/rfc/rfc3066.txt">IETF RFC 3066</a> limited to 2-letter language code per ISO standard
126: * 639, a "-" (dash) and a 2-letter country code per ISO 3166 alpha-2 country codes. E.g. "en-US" for American
127: * English, "en-GB" for British English, etc.
128: *
129: * @return a <a href="http://www.ietf.org/rfc/rfc3066.txt">IETF RFC 3066</a>-compatible language tag.
130: * @throws IllegalArgumentException if the given locale is not valid
131: * @since 2.4
132: */
133: public String getRFC3066LanguageTag() {
134: return rfc3066LangageTag;
135: }
136:
137: /**
138: * Retrieves the language identification tag associated to the specified Locale as defined by <a
139: * href="http://www.ietf.org/rfc/rfc3066.txt">IETF RFC 3066</a> limited to 2-letter language code per ISO standard
140: * 639, a "-" (dash) and a 2-letter country code per ISO 3166 alpha-2 country codes. E.g. "en-US" for American
141: * English, "en-GB" for British English, etc.
142: *
143: * @param locale the locale which language tag is wanted
144: * @return a <a href="http://www.ietf.org/rfc/rfc3066.txt">IETF RFC 3066</a>-compatible language tag.
145: * @throws IllegalArgumentException if the given locale is not valid
146: * @since 2.4
147: */
148: public static String getRFC3066LanguageTagFor(Locale locale) {
149: String country = locale.getCountry(); // country will be empty if no country was specified in the locale
150: return locale.getLanguage()
151: + ((country.length() == 2) ? RFC3066_SEPARATOR
152: + country : country);
153: }
154:
155: // Object override **************************************************************************************************
156:
157: public String toString() {
158: return locale.toString();
159: }
160:
161: // Static ***********************************************************************************************************
162:
163: /**
164: * Compute the trailing name for a given locale.
165: *
166: * @param locale the locale
167: * @return the trailing name
168: * @throws IllegalArgumentException if locale is null
169: */
170: public static String computeTrailingName(Locale locale)
171: throws IllegalArgumentException {
172: if (locale == null) {
173: throw new IllegalArgumentException(
174: "locale parameter is null");
175: }
176: StringBuffer tmp = new StringBuffer();
177: if (locale.getLanguage() != null
178: && locale.getLanguage().length() > 0) {
179: tmp.append('_').append(locale.getLanguage());
180: if (locale.getCountry() != null
181: && locale.getCountry().length() > 0) {
182: tmp.append('_').append(locale.getCountry());
183: {
184: if (locale.getVariant() != null
185: && locale.getVariant().length() > 0) {
186: tmp.append('_').append(locale.getVariant());
187: }
188: }
189: }
190: }
191: return tmp.toString();
192: }
193:
194: /**
195: * <p>Get an info object.</p> <p/> <p>Whenever the info object cannot be found then it creates a new one and possibly
196: * several since each info object may have a non null parent.</p> <p/> <p>If the infos argument is null, then the
197: * static shared infos is used to perform a lookup. If the lookup returns null then the shared infos is cloned and
198: * updated with the newly created info objects. The operation performed is a copy on write and is thread safe. Note
199: * that concurrent updates may cause the lost of info object in the shared infos.</p> <p/> <p>If the infos argument
200: * is not null, then it is used to perform a lookup first. If the lookup returns null then the infos object udpated
201: * with the newly created info objects. Note that no cloning of the infos object is performed.</p>
202: *
203: * @param locale the locale
204: * @param infos the infos
205: * @return A info object for the given locale
206: */
207: private static LocaleInfo getInfo(Locale locale, final Infos infos) {
208: // Perform a lookup first
209: Infos workInfos = null;
210: if (infos != null) {
211: workInfos = infos;
212: } else {
213: workInfos = sharedInfos;
214: }
215: LocaleInfo info = (LocaleInfo) workInfos.byLocale.get(locale
216: .toString());
217:
218: // If the lookup fail we create a new object
219: if (info == null) {
220: // Clone if it is shared
221: if (infos == null) {
222: workInfos = new Infos(workInfos);
223: }
224:
225: // Perform parent resolution
226: LocaleInfo parent = null;
227: if (locale.getVariant() != null
228: && locale.getVariant().length() > 0) {
229: // If this is a language/country/variant, try to get language/country
230: Locale parentLocale = new Locale(locale.getLanguage(),
231: locale.getCountry());
232: parent = getInfo(parentLocale, workInfos);
233: } else if (locale.getCountry() != null
234: && locale.getCountry().length() > 0) {
235: // If this is a language/country, try to get language
236: Locale parentLocale = new Locale(locale.getLanguage());
237: parent = getInfo(parentLocale, workInfos);
238: } else {
239: // When it is language then the parent is null
240: parent = null;
241: }
242:
243: //
244: info = new LocaleInfo(parent, locale);
245: log.debug("LocaleInfo " + locale.toString()
246: + " initialized");
247:
248: // Add to map
249: workInfos.byLocale.put(info.getLocale(), info);
250: workInfos.byLocaleCode.put(info.getLocale().toString(),
251: info);
252: workInfos.byRFC3066LanguageTag.put(info
253: .getRFC3066LanguageTag(), info);
254: workInfos.list.add(info);
255: // Collections.sort(workInfos.list, c);
256:
257: // Replace if we have cloned
258: if (infos == null) {
259: sharedInfos = workInfos;
260: }
261: }
262: return info;
263: }
264:
265: /** Create an infos object from the set of locales returned by <code>Locale.getAvailableLocales()</code>. */
266: private static Infos createFromAvailableLocales() {
267: // Initialize all objects
268: Infos workInfos = new Infos();
269: Locale[] temp = Locale.getAvailableLocales();
270: for (int i = 0; i < temp.length; i++) {
271: Locale locale = temp[i];
272: getInfo(locale, workInfos);
273: }
274: return workInfos;
275: }
276:
277: /** Return a collection of all available locale info for the platform. */
278: public static Collection getAll() {
279: return sharedInfos.unmodifiableList;
280: }
281:
282: /**
283: * Return the locale info for a specific code.
284: *
285: * @throws IllegalArgumentException if the locale info is not found for the code
286: */
287: public static LocaleInfo decodeLocaleInfo(String code)
288: throws IllegalArgumentException {
289: LocaleInfo info = (LocaleInfo) sharedInfos.byLocaleCode
290: .get(code);
291: if (info == null) {
292: throw new IllegalArgumentException(
293: "No locale info found for " + code);
294: }
295: return info;
296: }
297:
298: /**
299: * Return the locale info for a specific locale.
300: *
301: * @throws IllegalArgumentException if the locale is not found for the code
302: */
303: public static LocaleInfo decodeLocaleInfo(Locale locale) {
304: LocaleInfo info = (LocaleInfo) sharedInfos.byLocale.get(locale);
305: if (info == null) {
306: throw new IllegalArgumentException(
307: "No locale info found for " + locale);
308: }
309: return info;
310: }
311:
312: /**
313: * Returns an info object based on the given language identification tag as defined by <a
314: * href="http://www.ietf.org/rfc/rfc3066.txt">IETF RFC 3066</a> limited to 2-letter language code per ISO standard
315: * 639, a "-" (dash) and a 2-letter country code per ISO 3166 alpha-2 country codes. E.g. "en-US" for American
316: * English, "en-GB" for British English, etc.
317: *
318: * @param languageIdentificationTag the language tag to build the Locale from
319: * @return a LocaleInfo associated to the given compound language tag
320: * @throws IllegalArgumentException if the given compound language is not a valid one or null
321: * @since 2.4
322: */
323: public static LocaleInfo decodeLocaleInfoFromRFC3066LanguageTag(
324: String languageIdentificationTag) {
325: if (languageIdentificationTag == null) {
326: throw new IllegalArgumentException(
327: "Null language identification tag not accepted");
328: }
329:
330: LocaleInfo info = (LocaleInfo) sharedInfos.byRFC3066LanguageTag
331: .get(languageIdentificationTag);
332: if (info == null) {
333: Matcher matcher = RFC3066_COMPOUND_LANG_PATTERN
334: .matcher(languageIdentificationTag);
335: if (matcher.matches()) {
336: String language = matcher.group(1);
337: if (Arrays.binarySearch(SORTED_ISO_LANGUAGES, language) < 0) {
338: throw new IllegalArgumentException(
339: "Invalid ISO language code: " + language);
340: }
341: String country = matcher.group(3);
342: if (country != null
343: && Arrays.binarySearch(SORTED_ISO_COUNTRIES,
344: country) < 0) {
345: throw new IllegalArgumentException(
346: "Invalid ISO country code: " + country);
347: }
348: return new LocaleInfo(LocaleInfo
349: .decodeLocaleInfo(language), new Locale(
350: language, country));
351: }
352: throw new IllegalArgumentException(
353: languageIdentificationTag
354: + " is not a valid compound language : accepted "
355: + "format is xx-YY where xx is a valid ISO language code and YY is a valid country code. See "
356: + "java.util.Locale javadoc for more info.");
357: }
358: return info;
359: }
360:
361: /** Hold the several ways to retrieve infos object. */
362: private static class Infos {
363: /** The info by locale. */
364: private Map byLocale;
365:
366: /** The info by locale#toString() code. */
367: private Map byLocaleCode;
368:
369: /** The info by locale RFC3066 language tag. */
370: private Map byRFC3066LanguageTag;
371:
372: /** All the infos. */
373: private List list;
374:
375: /** All the infos as an unmodifiable list. */
376: private List unmodifiableList;
377:
378: /** Construct a new info object. */
379: public Infos() {
380: this .byLocale = new HashMap();
381: this .byLocaleCode = new HashMap();
382: this .byRFC3066LanguageTag = new HashMap();
383: this .list = new ArrayList();
384: this .unmodifiableList = Collections.unmodifiableList(list);
385: }
386:
387: /** Clone the info object state passed as argument. */
388: public Infos(Infos that) {
389: this .byLocale = new HashMap(that.byLocale);
390: this .byLocaleCode = new HashMap(that.byLocaleCode);
391: this .byRFC3066LanguageTag = new HashMap(
392: that.byRFC3066LanguageTag);
393: this .list = new ArrayList(that.list);
394: this.unmodifiableList = Collections.unmodifiableList(list);
395: }
396: }
397: }
|