• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 package org.apache.commons.lang3;
18 
19 import java.util.ArrayList;
20 import java.util.Arrays;
21 import java.util.Collections;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Locale;
25 import java.util.Set;
26 import java.util.concurrent.ConcurrentHashMap;
27 import java.util.concurrent.ConcurrentMap;
28 import java.util.function.Predicate;
29 import java.util.stream.Collectors;
30 
31 /**
32  * Operations to assist when working with a {@link Locale}.
33  *
34  * <p>This class tries to handle {@code null} input gracefully.
35  * An exception will not be thrown for a {@code null} input.
36  * Each method documents its behavior in more detail.</p>
37  *
38  * @since 2.2
39  */
40 public class LocaleUtils {
41     private static final char UNDERSCORE = '_';
42     private static final char DASH = '-';
43 
44     // class to avoid synchronization (Init on demand)
45     static class SyncAvoid {
46         /** Unmodifiable list of available locales. */
47         private static final List<Locale> AVAILABLE_LOCALE_LIST;
48         /** Unmodifiable set of available locales. */
49         private static final Set<Locale> AVAILABLE_LOCALE_SET;
50 
51         static {
52             final List<Locale> list = new ArrayList<>(Arrays.asList(Locale.getAvailableLocales()));  // extra safe
53             AVAILABLE_LOCALE_LIST = Collections.unmodifiableList(list);
54             AVAILABLE_LOCALE_SET = Collections.unmodifiableSet(new HashSet<>(list));
55         }
56     }
57 
58     /** Concurrent map of language locales by country. */
59     private static final ConcurrentMap<String, List<Locale>> cLanguagesByCountry =
60         new ConcurrentHashMap<>();
61 
62     /** Concurrent map of country locales by language. */
63     private static final ConcurrentMap<String, List<Locale>> cCountriesByLanguage =
64         new ConcurrentHashMap<>();
65 
66     /**
67      * Obtains an unmodifiable list of installed locales.
68      *
69      * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
70      * It is more efficient, as the JDK method must create a new array each
71      * time it is called.</p>
72      *
73      * @return the unmodifiable list of available locales
74      */
availableLocaleList()75     public static List<Locale> availableLocaleList() {
76         return SyncAvoid.AVAILABLE_LOCALE_LIST;
77     }
78 
availableLocaleList(final Predicate<Locale> predicate)79     private static List<Locale> availableLocaleList(final Predicate<Locale> predicate) {
80         return availableLocaleList().stream().filter(predicate).collect(Collectors.toList());
81     }
82 
83     /**
84      * Obtains an unmodifiable set of installed locales.
85      *
86      * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
87      * It is more efficient, as the JDK method must create a new array each
88      * time it is called.</p>
89      *
90      * @return the unmodifiable set of available locales
91      */
availableLocaleSet()92     public static Set<Locale> availableLocaleSet() {
93         return SyncAvoid.AVAILABLE_LOCALE_SET;
94     }
95 
96     /**
97      * Obtains the list of countries supported for a given language.
98      *
99      * <p>This method takes a language code and searches to find the
100      * countries available for that language. Variant locales are removed.</p>
101      *
102      * @param languageCode  the 2 letter language code, null returns empty
103      * @return an unmodifiable List of Locale objects, not null
104      */
countriesByLanguage(final String languageCode)105     public static List<Locale> countriesByLanguage(final String languageCode) {
106         if (languageCode == null) {
107             return Collections.emptyList();
108         }
109         return cCountriesByLanguage.computeIfAbsent(languageCode, lc -> Collections.unmodifiableList(
110             availableLocaleList(locale -> languageCode.equals(locale.getLanguage()) && !locale.getCountry().isEmpty() && locale.getVariant().isEmpty())));
111     }
112 
113     /**
114      * Checks if the locale specified is in the set of available locales.
115      *
116      * @param locale the Locale object to check if it is available
117      * @return true if the locale is a known locale
118      */
isAvailableLocale(final Locale locale)119     public static boolean isAvailableLocale(final Locale locale) {
120         return availableLocaleSet().contains(locale);
121     }
122 
123     /**
124      * Checks whether the given String is a ISO 3166 alpha-2 country code.
125      *
126      * @param str the String to check
127      * @return true, is the given String is a ISO 3166 compliant country code.
128      */
isISO3166CountryCode(final String str)129     private static boolean isISO3166CountryCode(final String str) {
130         return StringUtils.isAllUpperCase(str) && str.length() == 2;
131     }
132 
133     /**
134      * Checks whether the given String is a ISO 639 compliant language code.
135      *
136      * @param str the String to check.
137      * @return true, if the given String is a ISO 639 compliant language code.
138      */
isISO639LanguageCode(final String str)139     private static boolean isISO639LanguageCode(final String str) {
140         return StringUtils.isAllLowerCase(str) && (str.length() == 2 || str.length() == 3);
141     }
142 
143     /**
144      * Checks whether the given String is a UN M.49 numeric area code.
145      *
146      * @param str the String to check
147      * @return true, is the given String is a UN M.49 numeric area code.
148      */
isNumericAreaCode(final String str)149     private static boolean isNumericAreaCode(final String str) {
150         return StringUtils.isNumeric(str) && str.length() == 3;
151     }
152 
153     /**
154      * Obtains the list of languages supported for a given country.
155      *
156      * <p>This method takes a country code and searches to find the
157      * languages available for that country. Variant locales are removed.</p>
158      *
159      * @param countryCode  the 2-letter country code, null returns empty
160      * @return an unmodifiable List of Locale objects, not null
161      */
languagesByCountry(final String countryCode)162     public static List<Locale> languagesByCountry(final String countryCode) {
163         if (countryCode == null) {
164             return Collections.emptyList();
165         }
166         return cLanguagesByCountry.computeIfAbsent(countryCode,
167             k -> Collections.unmodifiableList(availableLocaleList(locale -> countryCode.equals(locale.getCountry()) && locale.getVariant().isEmpty())));
168     }
169 
170     /**
171      * Obtains the list of locales to search through when performing
172      * a locale search.
173      *
174      * <pre>
175      * localeLookupList(Locale("fr", "CA", "xxx"))
176      *   = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr")]
177      * </pre>
178      *
179      * @param locale  the locale to start from
180      * @return the unmodifiable list of Locale objects, 0 being locale, not null
181      */
localeLookupList(final Locale locale)182     public static List<Locale> localeLookupList(final Locale locale) {
183         return localeLookupList(locale, locale);
184     }
185 
186     /**
187      * Obtains the list of locales to search through when performing
188      * a locale search.
189      *
190      * <pre>
191      * localeLookupList(Locale("fr", "CA", "xxx"), Locale("en"))
192      *   = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr"), Locale("en"]
193      * </pre>
194      *
195      * <p>The result list begins with the most specific locale, then the
196      * next more general and so on, finishing with the default locale.
197      * The list will never contain the same locale twice.</p>
198      *
199      * @param locale  the locale to start from, null returns empty list
200      * @param defaultLocale  the default locale to use if no other is found
201      * @return the unmodifiable list of Locale objects, 0 being locale, not null
202      */
localeLookupList(final Locale locale, final Locale defaultLocale)203     public static List<Locale> localeLookupList(final Locale locale, final Locale defaultLocale) {
204         final List<Locale> list = new ArrayList<>(4);
205         if (locale != null) {
206             list.add(locale);
207             if (!locale.getVariant().isEmpty()) {
208                 list.add(new Locale(locale.getLanguage(), locale.getCountry()));
209             }
210             if (!locale.getCountry().isEmpty()) {
211                 list.add(new Locale(locale.getLanguage(), StringUtils.EMPTY));
212             }
213             if (!list.contains(defaultLocale)) {
214                 list.add(defaultLocale);
215             }
216         }
217         return Collections.unmodifiableList(list);
218     }
219 
220     /**
221      * Tries to parse a locale from the given String.
222      *
223      * @param str the String to parse a locale from.
224      * @return a Locale instance parsed from the given String.
225      * @throws IllegalArgumentException if the given String can not be parsed.
226      */
parseLocale(final String str)227     private static Locale parseLocale(final String str) {
228         if (isISO639LanguageCode(str)) {
229             return new Locale(str);
230         }
231 
232         final String[] segments = str.indexOf(UNDERSCORE) != -1
233             ? str.split(String.valueOf(UNDERSCORE), -1)
234             : str.split(String.valueOf(DASH), -1);
235         final String language = segments[0];
236         if (segments.length == 2) {
237             final String country = segments[1];
238             if (isISO639LanguageCode(language) && isISO3166CountryCode(country) ||
239                     isNumericAreaCode(country)) {
240                 return new Locale(language, country);
241             }
242         } else if (segments.length == 3) {
243             final String country = segments[1];
244             final String variant = segments[2];
245             if (isISO639LanguageCode(language) &&
246                     (country.isEmpty() || isISO3166CountryCode(country) || isNumericAreaCode(country)) &&
247                     !variant.isEmpty()) {
248                 return new Locale(language, country, variant);
249             }
250         }
251         throw new IllegalArgumentException("Invalid locale format: " + str);
252     }
253 
254     /**
255      * Returns the given locale if non-{@code null}, otherwise {@link Locale#getDefault()}.
256      *
257      * @param locale a locale or {@code null}.
258      * @return the given locale if non-{@code null}, otherwise {@link Locale#getDefault()}.
259      * @since 3.12.0
260      */
toLocale(final Locale locale)261     public static Locale toLocale(final Locale locale) {
262         return locale != null ? locale : Locale.getDefault();
263     }
264 
265     /**
266      * Converts a String to a Locale.
267      *
268      * <p>This method takes the string format of a locale and creates the
269      * locale object from it.</p>
270      *
271      * <pre>
272      *   LocaleUtils.toLocale("")           = new Locale("", "")
273      *   LocaleUtils.toLocale("en")         = new Locale("en", "")
274      *   LocaleUtils.toLocale("en_GB")      = new Locale("en", "GB")
275      *   LocaleUtils.toLocale("en-GB")      = new Locale("en", "GB")
276      *   LocaleUtils.toLocale("en_001")     = new Locale("en", "001")
277      *   LocaleUtils.toLocale("en_GB_xxx")  = new Locale("en", "GB", "xxx")   (#)
278      * </pre>
279      *
280      * <p>(#) The behavior of the JDK variant constructor changed between JDK1.3 and JDK1.4.
281      * In JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't.
282      * Thus, the result from getVariant() may vary depending on your JDK.</p>
283      *
284      * <p>This method validates the input strictly.
285      * The language code must be lowercase.
286      * The country code must be uppercase.
287      * The separator must be an underscore or a dash.
288      * The length must be correct.
289      * </p>
290      *
291      * @param str  the locale String to convert, null returns null
292      * @return a Locale, null if null input
293      * @throws IllegalArgumentException if the string is an invalid format
294      * @see Locale#forLanguageTag(String)
295      */
toLocale(final String str)296     public static Locale toLocale(final String str) {
297         if (str == null) {
298             // TODO Should this return the default locale?
299             return null;
300         }
301         if (str.isEmpty()) { // LANG-941 - JDK 8 introduced an empty locale where all fields are blank
302             return new Locale(StringUtils.EMPTY, StringUtils.EMPTY);
303         }
304         if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions
305             throw new IllegalArgumentException("Invalid locale format: " + str);
306         }
307         final int len = str.length();
308         if (len < 2) {
309             throw new IllegalArgumentException("Invalid locale format: " + str);
310         }
311         final char ch0 = str.charAt(0);
312         if (ch0 == UNDERSCORE || ch0 == DASH) {
313             if (len < 3) {
314                 throw new IllegalArgumentException("Invalid locale format: " + str);
315             }
316             final char ch1 = str.charAt(1);
317             final char ch2 = str.charAt(2);
318             if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) {
319                 throw new IllegalArgumentException("Invalid locale format: " + str);
320             }
321             if (len == 3) {
322                 return new Locale(StringUtils.EMPTY, str.substring(1, 3));
323             }
324             if (len < 5) {
325                 throw new IllegalArgumentException("Invalid locale format: " + str);
326             }
327             if (str.charAt(3) != ch0) {
328                 throw new IllegalArgumentException("Invalid locale format: " + str);
329             }
330             return new Locale(StringUtils.EMPTY, str.substring(1, 3), str.substring(4));
331         }
332 
333         return parseLocale(str);
334     }
335 
336     /**
337      * {@link LocaleUtils} instances should NOT be constructed in standard programming.
338      * Instead, the class should be used as {@code LocaleUtils.toLocale("en_GB");}.
339      *
340      * <p>This constructor is public to permit tools that require a JavaBean instance
341      * to operate.</p>
342      */
LocaleUtils()343     public LocaleUtils() {
344     }
345 
346 }
347