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    }