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 Stephen Colebourne
037     * @since 2.2
038     * @version $Id: LocaleUtils.java 534277 2007-05-01 23:50:01Z bayard $
039     */
040    public class LocaleUtils {
041    
042        /** Unmodifiable list of available locales. */
043        private static final List cAvailableLocaleList;
044        /** Unmodifiable set of available locales. */
045        private static Set cAvailableLocaleSet;
046        /** Unmodifiable map of language locales by country. */
047        private static final Map cLanguagesByCountry = Collections.synchronizedMap(new HashMap());
048        /** Unmodifiable map of country locales by language. */
049        private static final Map cCountriesByLanguage = Collections.synchronizedMap(new HashMap());
050        static {
051            List list = Arrays.asList(Locale.getAvailableLocales());
052            cAvailableLocaleList = Collections.unmodifiableList(list);
053        }
054    
055        /**
056         * <p><code>LocaleUtils</code> instances should NOT be constructed in standard programming.
057         * Instead, the class should be used as <code>LocaleUtils.toLocale("en_GB");</code>.</p>
058         *
059         * <p>This constructor is public to permit tools that require a JavaBean instance
060         * to operate.</p>
061         */
062        public LocaleUtils() {
063          super();
064        }
065    
066        //-----------------------------------------------------------------------
067        /**
068         * <p>Converts a String to a Locale.</p>
069         *
070         * <p>This method takes the string format of a locale and creates the
071         * locale object from it.</p>
072         *
073         * <pre>
074         *   LocaleUtils.toLocale("en")         = new Locale("en", "")
075         *   LocaleUtils.toLocale("en_GB")      = new Locale("en", "GB")
076         *   LocaleUtils.toLocale("en_GB_xxx")  = new Locale("en", "GB", "xxx")   (#)
077         * </pre>
078         *
079         * <p>(#) The behaviour of the JDK variant constructor changed between JDK1.3 and JDK1.4.
080         * In JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't.
081         * Thus, the result from getVariant() may vary depending on your JDK.</p>
082         *
083         * <p>This method validates the input strictly.
084         * The language code must be lowercase.
085         * The country code must be uppercase.
086         * The separator must be an underscore.
087         * The length must be correct.
088         * </p>
089         *
090         * @param str  the locale String to convert, null returns null
091         * @return a Locale, null if null input
092         * @throws IllegalArgumentException if the string is an invalid format
093         */
094        public static Locale toLocale(String str) {
095            if (str == null) {
096                return null;
097            }
098            int len = str.length();
099            if (len != 2 && len != 5 && len < 7) {
100                throw new IllegalArgumentException("Invalid locale format: " + str);
101            }
102            char ch0 = str.charAt(0);
103            char ch1 = str.charAt(1);
104            if (ch0 < 'a' || ch0 > 'z' || ch1 < 'a' || ch1 > 'z') {
105                throw new IllegalArgumentException("Invalid locale format: " + str);
106            }
107            if (len == 2) {
108                return new Locale(str, "");
109            } else {
110                if (str.charAt(2) != '_') {
111                    throw new IllegalArgumentException("Invalid locale format: " + str);
112                }
113                char ch3 = str.charAt(3);
114                if (ch3 == '_') {
115                    return new Locale(str.substring(0, 2), "", str.substring(4));
116                }
117                char ch4 = str.charAt(4);
118                if (ch3 < 'A' || ch3 > 'Z' || ch4 < 'A' || ch4 > 'Z') {
119                    throw new IllegalArgumentException("Invalid locale format: " + str);
120                }
121                if (len == 5) {
122                    return new Locale(str.substring(0, 2), str.substring(3, 5));
123                } else {
124                    if (str.charAt(5) != '_') {
125                        throw new IllegalArgumentException("Invalid locale format: " + str);
126                    }
127                    return new Locale(str.substring(0, 2), str.substring(3, 5), str.substring(6));
128                }
129            }
130        }
131    
132        //-----------------------------------------------------------------------
133        /**
134         * <p>Obtains the list of locales to search through when performing
135         * a locale search.</p>
136         *
137         * <pre>
138         * localeLookupList(Locale("fr","CA","xxx"))
139         *   = [Locale("fr","CA","xxx"), Locale("fr","CA"), Locale("fr")]
140         * </pre>
141         *
142         * @param locale  the locale to start from
143         * @return the unmodifiable list of Locale objects, 0 being locale, never null
144         */
145        public static List localeLookupList(Locale locale) {
146            return localeLookupList(locale, locale);
147        }
148    
149        //-----------------------------------------------------------------------
150        /**
151         * <p>Obtains the list of locales to search through when performing
152         * a locale search.</p>
153         *
154         * <pre>
155         * localeLookupList(Locale("fr", "CA", "xxx"), Locale("en"))
156         *   = [Locale("fr","CA","xxx"), Locale("fr","CA"), Locale("fr"), Locale("en"]
157         * </pre>
158         *
159         * <p>The result list begins with the most specific locale, then the
160         * next more general and so on, finishing with the default locale.
161         * The list will never contain the same locale twice.</p>
162         *
163         * @param locale  the locale to start from, null returns empty list
164         * @param defaultLocale  the default locale to use if no other is found
165         * @return the unmodifiable list of Locale objects, 0 being locale, never null
166         */
167        public static List localeLookupList(Locale locale, Locale defaultLocale) {
168            List list = new ArrayList(4);
169            if (locale != null) {
170                list.add(locale);
171                if (locale.getVariant().length() > 0) {
172                    list.add(new Locale(locale.getLanguage(), locale.getCountry()));
173                }
174                if (locale.getCountry().length() > 0) {
175                    list.add(new Locale(locale.getLanguage(), ""));
176                }
177                if (list.contains(defaultLocale) == false) {
178                    list.add(defaultLocale);
179                }
180            }
181            return Collections.unmodifiableList(list);
182        }
183    
184        //-----------------------------------------------------------------------
185        /**
186         * <p>Obtains an unmodifiable list of installed locales.</p>
187         * 
188         * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
189         * It is more efficient, as the JDK method must create a new array each
190         * time it is called.</p>
191         *
192         * @return the unmodifiable list of available locales
193         */
194        public static List availableLocaleList() {
195            return cAvailableLocaleList;
196        }
197    
198        //-----------------------------------------------------------------------
199        /**
200         * <p>Obtains an unmodifiable set of installed locales.</p>
201         * 
202         * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
203         * It is more efficient, as the JDK method must create a new array each
204         * time it is called.</p>
205         *
206         * @return the unmodifiable set of available locales
207         */
208        public static Set availableLocaleSet() {
209            Set set = cAvailableLocaleSet;
210            if (set == null) {
211                set = new HashSet(availableLocaleList());
212                set = Collections.unmodifiableSet(set);
213                cAvailableLocaleSet = set;
214            }
215            return set;
216        }
217    
218        //-----------------------------------------------------------------------
219        /**
220         * <p>Checks if the locale specified is in the list of available locales.</p>
221         *
222         * @param locale the Locale object to check if it is available
223         * @return true if the locale is a known locale
224         */
225        public static boolean isAvailableLocale(Locale locale) {
226            return availableLocaleList().contains(locale);
227        }
228    
229        //-----------------------------------------------------------------------
230        /**
231         * <p>Obtains the list of languages supported for a given country.</p>
232         *
233         * <p>This method takes a country code and searches to find the
234         * languages available for that country. Variant locales are removed.</p>
235         *
236         * @param countryCode  the 2 letter country code, null returns empty
237         * @return an unmodifiable List of Locale objects, never null
238         */
239        public static List languagesByCountry(String countryCode) {
240            List langs = (List) cLanguagesByCountry.get(countryCode);  //syncd
241            if (langs == null) {
242                if (countryCode != null) {
243                    langs = new ArrayList();
244                    List locales = availableLocaleList();
245                    for (int i = 0; i < locales.size(); i++) {
246                        Locale locale = (Locale) locales.get(i);
247                        if (countryCode.equals(locale.getCountry()) &&
248                                locale.getVariant().length() == 0) {
249                            langs.add(locale);
250                        }
251                    }
252                    langs = Collections.unmodifiableList(langs);
253                } else {
254                    langs = Collections.EMPTY_LIST;
255                }
256                cLanguagesByCountry.put(countryCode, langs);  //syncd
257            }
258            return langs;
259        }
260    
261        //-----------------------------------------------------------------------
262        /**
263         * <p>Obtains the list of countries supported for a given language.</p>
264         * 
265         * <p>This method takes a language code and searches to find the
266         * countries available for that language. Variant locales are removed.</p>
267         *
268         * @param languageCode  the 2 letter language code, null returns empty
269         * @return an unmodifiable List of Locale objects, never null
270         */
271        public static List countriesByLanguage(String languageCode) {
272            List countries = (List) cCountriesByLanguage.get(languageCode);  //syncd
273            if (countries == null) {
274                if (languageCode != null) {
275                    countries = new ArrayList();
276                    List locales = availableLocaleList();
277                    for (int i = 0; i < locales.size(); i++) {
278                        Locale locale = (Locale) locales.get(i);
279                        if (languageCode.equals(locale.getLanguage()) &&
280                                locale.getCountry().length() != 0 &&
281                                locale.getVariant().length() == 0) {
282                            countries.add(locale);
283                        }
284                    }
285                    countries = Collections.unmodifiableList(countries);
286                } else {
287                    countries = Collections.EMPTY_LIST;
288                }
289                cCountriesByLanguage.put(languageCode, countries);  //syncd
290            }
291            return countries;
292        }
293    
294    }