001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.commons.lang;
018
019 import java.util.ArrayList;
020 import java.util.Arrays;
021 import java.util.Collections;
022 import java.util.HashMap;
023 import java.util.HashSet;
024 import java.util.List;
025 import java.util.Locale;
026 import java.util.Map;
027 import java.util.Set;
028
029 /**
030 * <p>Operations to assist when working with a {@link Locale}.</p>
031 *
032 * <p>This class tries to handle <code>null</code> input gracefully.
033 * An exception will not be thrown for a <code>null</code> input.
034 * Each method documents its behaviour in more detail.</p>
035 *
036 * @author Apache Software Foundation
037 * @since 2.2
038 * @version $Id: LocaleUtils.java 911968 2010-02-19 20:26:21Z niallp $
039 */
040 public class LocaleUtils {
041
042 /** Unmodifiable list of available locales. */
043 private static List cAvailableLocaleList; // lazily created by availableLocaleList()
044
045 /** Unmodifiable set of available locales. */
046 private static Set cAvailableLocaleSet; // lazily created by availableLocaleSet()
047
048 /** Unmodifiable map of language locales by country. */
049 private static final Map cLanguagesByCountry = Collections.synchronizedMap(new HashMap());
050
051 /** Unmodifiable map of country locales by language. */
052 private static final Map cCountriesByLanguage = Collections.synchronizedMap(new HashMap());
053
054 /**
055 * <p><code>LocaleUtils</code> instances should NOT be constructed in standard programming.
056 * Instead, the class should be used as <code>LocaleUtils.toLocale("en_GB");</code>.</p>
057 *
058 * <p>This constructor is public to permit tools that require a JavaBean instance
059 * to operate.</p>
060 */
061 public LocaleUtils() {
062 super();
063 }
064
065 //-----------------------------------------------------------------------
066 /**
067 * <p>Converts a String to a Locale.</p>
068 *
069 * <p>This method takes the string format of a locale and creates the
070 * locale object from it.</p>
071 *
072 * <pre>
073 * LocaleUtils.toLocale("en") = new Locale("en", "")
074 * LocaleUtils.toLocale("en_GB") = new Locale("en", "GB")
075 * LocaleUtils.toLocale("en_GB_xxx") = new Locale("en", "GB", "xxx") (#)
076 * </pre>
077 *
078 * <p>(#) The behaviour of the JDK variant constructor changed between JDK1.3 and JDK1.4.
079 * In JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't.
080 * Thus, the result from getVariant() may vary depending on your JDK.</p>
081 *
082 * <p>This method validates the input strictly.
083 * The language code must be lowercase.
084 * The country code must be uppercase.
085 * The separator must be an underscore.
086 * The length must be correct.
087 * </p>
088 *
089 * @param str the locale String to convert, null returns null
090 * @return a Locale, null if null input
091 * @throws IllegalArgumentException if the string is an invalid format
092 */
093 public static Locale toLocale(String str) {
094 if (str == null) {
095 return null;
096 }
097 int len = str.length();
098 if (len != 2 && len != 5 && len < 7) {
099 throw new IllegalArgumentException("Invalid locale format: " + str);
100 }
101 char ch0 = str.charAt(0);
102 char ch1 = str.charAt(1);
103 if (ch0 < 'a' || ch0 > 'z' || ch1 < 'a' || ch1 > 'z') {
104 throw new IllegalArgumentException("Invalid locale format: " + str);
105 }
106 if (len == 2) {
107 return new Locale(str, "");
108 } else {
109 if (str.charAt(2) != '_') {
110 throw new IllegalArgumentException("Invalid locale format: " + str);
111 }
112 char ch3 = str.charAt(3);
113 if (ch3 == '_') {
114 return new Locale(str.substring(0, 2), "", str.substring(4));
115 }
116 char ch4 = str.charAt(4);
117 if (ch3 < 'A' || ch3 > 'Z' || ch4 < 'A' || ch4 > 'Z') {
118 throw new IllegalArgumentException("Invalid locale format: " + str);
119 }
120 if (len == 5) {
121 return new Locale(str.substring(0, 2), str.substring(3, 5));
122 } else {
123 if (str.charAt(5) != '_') {
124 throw new IllegalArgumentException("Invalid locale format: " + str);
125 }
126 return new Locale(str.substring(0, 2), str.substring(3, 5), str.substring(6));
127 }
128 }
129 }
130
131 //-----------------------------------------------------------------------
132 /**
133 * <p>Obtains the list of locales to search through when performing
134 * a locale search.</p>
135 *
136 * <pre>
137 * localeLookupList(Locale("fr","CA","xxx"))
138 * = [Locale("fr","CA","xxx"), Locale("fr","CA"), Locale("fr")]
139 * </pre>
140 *
141 * @param locale the locale to start from
142 * @return the unmodifiable list of Locale objects, 0 being locale, never null
143 */
144 public static List localeLookupList(Locale locale) {
145 return localeLookupList(locale, locale);
146 }
147
148 //-----------------------------------------------------------------------
149 /**
150 * <p>Obtains the list of locales to search through when performing
151 * a locale search.</p>
152 *
153 * <pre>
154 * localeLookupList(Locale("fr", "CA", "xxx"), Locale("en"))
155 * = [Locale("fr","CA","xxx"), Locale("fr","CA"), Locale("fr"), Locale("en"]
156 * </pre>
157 *
158 * <p>The result list begins with the most specific locale, then the
159 * next more general and so on, finishing with the default locale.
160 * The list will never contain the same locale twice.</p>
161 *
162 * @param locale the locale to start from, null returns empty list
163 * @param defaultLocale the default locale to use if no other is found
164 * @return the unmodifiable list of Locale objects, 0 being locale, never null
165 */
166 public static List localeLookupList(Locale locale, Locale defaultLocale) {
167 List list = new ArrayList(4);
168 if (locale != null) {
169 list.add(locale);
170 if (locale.getVariant().length() > 0) {
171 list.add(new Locale(locale.getLanguage(), locale.getCountry()));
172 }
173 if (locale.getCountry().length() > 0) {
174 list.add(new Locale(locale.getLanguage(), ""));
175 }
176 if (list.contains(defaultLocale) == false) {
177 list.add(defaultLocale);
178 }
179 }
180 return Collections.unmodifiableList(list);
181 }
182
183 //-----------------------------------------------------------------------
184 /**
185 * <p>Obtains an unmodifiable list of installed locales.</p>
186 *
187 * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
188 * It is more efficient, as the JDK method must create a new array each
189 * time it is called.</p>
190 *
191 * @return the unmodifiable list of available locales
192 */
193 public static List availableLocaleList() {
194 if(cAvailableLocaleList == null) {
195 initAvailableLocaleList();
196 }
197 return cAvailableLocaleList;
198 }
199
200 /**
201 * Initializes the availableLocaleList. It is separate from availableLocaleList()
202 * to avoid the synchronized block affecting normal use, yet synchronized and
203 * lazy loading to avoid a static block affecting other methods in this class.
204 */
205 private static synchronized void initAvailableLocaleList() {
206 if(cAvailableLocaleList == null) {
207 List list = Arrays.asList(Locale.getAvailableLocales());
208 cAvailableLocaleList = Collections.unmodifiableList(list);
209 }
210 }
211
212 //-----------------------------------------------------------------------
213 /**
214 * <p>Obtains an unmodifiable set of installed locales.</p>
215 *
216 * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
217 * It is more efficient, as the JDK method must create a new array each
218 * time it is called.</p>
219 *
220 * @return the unmodifiable set of available locales
221 */
222 public static Set availableLocaleSet() {
223 if(cAvailableLocaleSet == null) {
224 initAvailableLocaleSet();
225 }
226 return cAvailableLocaleSet;
227 }
228
229 /**
230 * Initializes the availableLocaleSet. It is separate from availableLocaleSet()
231 * to avoid the synchronized block affecting normal use, yet synchronized and
232 * lazy loading to avoid a static block affecting other methods in this class.
233 */
234 private static synchronized void initAvailableLocaleSet() {
235 if(cAvailableLocaleSet == null) {
236 cAvailableLocaleSet = Collections.unmodifiableSet( new HashSet(availableLocaleList()) );
237 }
238 }
239
240 //-----------------------------------------------------------------------
241 /**
242 * <p>Checks if the locale specified is in the list of available locales.</p>
243 *
244 * @param locale the Locale object to check if it is available
245 * @return true if the locale is a known locale
246 */
247 public static boolean isAvailableLocale(Locale locale) {
248 return availableLocaleList().contains(locale);
249 }
250
251 //-----------------------------------------------------------------------
252 /**
253 * <p>Obtains the list of languages supported for a given country.</p>
254 *
255 * <p>This method takes a country code and searches to find the
256 * languages available for that country. Variant locales are removed.</p>
257 *
258 * @param countryCode the 2 letter country code, null returns empty
259 * @return an unmodifiable List of Locale objects, never null
260 */
261 public static List languagesByCountry(String countryCode) {
262 List langs = (List) cLanguagesByCountry.get(countryCode); //syncd
263 if (langs == null) {
264 if (countryCode != null) {
265 langs = new ArrayList();
266 List locales = availableLocaleList();
267 for (int i = 0; i < locales.size(); i++) {
268 Locale locale = (Locale) locales.get(i);
269 if (countryCode.equals(locale.getCountry()) &&
270 locale.getVariant().length() == 0) {
271 langs.add(locale);
272 }
273 }
274 langs = Collections.unmodifiableList(langs);
275 } else {
276 langs = Collections.EMPTY_LIST;
277 }
278 cLanguagesByCountry.put(countryCode, langs); //syncd
279 }
280 return langs;
281 }
282
283 //-----------------------------------------------------------------------
284 /**
285 * <p>Obtains the list of countries supported for a given language.</p>
286 *
287 * <p>This method takes a language code and searches to find the
288 * countries available for that language. Variant locales are removed.</p>
289 *
290 * @param languageCode the 2 letter language code, null returns empty
291 * @return an unmodifiable List of Locale objects, never null
292 */
293 public static List countriesByLanguage(String languageCode) {
294 List countries = (List) cCountriesByLanguage.get(languageCode); //syncd
295 if (countries == null) {
296 if (languageCode != null) {
297 countries = new ArrayList();
298 List locales = availableLocaleList();
299 for (int i = 0; i < locales.size(); i++) {
300 Locale locale = (Locale) locales.get(i);
301 if (languageCode.equals(locale.getLanguage()) &&
302 locale.getCountry().length() != 0 &&
303 locale.getVariant().length() == 0) {
304 countries.add(locale);
305 }
306 }
307 countries = Collections.unmodifiableList(countries);
308 } else {
309 countries = Collections.EMPTY_LIST;
310 }
311 cCountriesByLanguage.put(languageCode, countries); //syncd
312 }
313 return countries;
314 }
315
316 }