View Javadoc
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    *      https://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  
18  package org.apache.commons.lang3;
19  
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collections;
23  import java.util.Comparator;
24  import java.util.LinkedHashSet;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Set;
28  import java.util.concurrent.ConcurrentHashMap;
29  import java.util.concurrent.ConcurrentMap;
30  import java.util.function.Predicate;
31  import java.util.stream.Collectors;
32  
33  /**
34   * Operations to assist when working with a {@link Locale}.
35   *
36   * <p>
37   * This class tries to handle {@code null} input gracefully. An exception will not be thrown for a {@code null} input. Each method documents its behavior in
38   * more detail.
39   * </p>
40   *
41   * @see Locale
42   * @since 2.2
43   */
44  public class LocaleUtils {
45  
46      /**
47       * Avoids synchronization, initializes on demand.
48       */
49      private static final class SyncAvoid {
50  
51          /** Private unmodifiable and sorted list of available locales. */
52          private static final List<Locale> AVAILABLE_LOCALE_ULIST;
53  
54          /** Private unmodifiable set of available locales. */
55          private static final Set<Locale> AVAILABLE_LOCALE_USET;
56          static {
57              AVAILABLE_LOCALE_ULIST = Collections
58                      .unmodifiableList(Arrays.asList(ArraySorter.sort(Locale.getAvailableLocales(), Comparator.comparing(Locale::toString))));
59              AVAILABLE_LOCALE_USET = Collections.unmodifiableSet(new LinkedHashSet<>(AVAILABLE_LOCALE_ULIST));
60          }
61      }
62  
63      /**
64       * The underscore character {@code '}{@value}{@code '}.
65       */
66      private static final char UNDERSCORE = '_';
67  
68      /**
69       * The undetermined language {@value}.
70       * <p>
71       * If a language is empty, or not <em>well-formed</em> (for example "a" or "e2"), {@link Locale#toLanguageTag()} will return {@code "und"} (Undetermined).
72       * </p>
73       *
74       * @see Locale#toLanguageTag()
75       */
76      private static final String UNDETERMINED = "und";
77  
78      /**
79       * The dash character {@code '}{@value}{@code '}.
80       */
81      private static final char DASH = '-';
82  
83      /**
84       * Concurrent map of language locales by country.
85       */
86      private static final ConcurrentMap<String, List<Locale>> ccToLocalesMap = new ConcurrentHashMap<>();
87  
88  
89      /**
90       * Concurrent map of country locales by language.
91       */
92      private static final ConcurrentMap<String, List<Locale>> lcToLocalesMap = new ConcurrentHashMap<>();
93  
94      /**
95       * Gets an unmodifiable and sorted list of installed locales.
96       *
97       * <p>
98       * This method is a wrapper around {@link Locale#getAvailableLocales()}. It is more efficient, as the JDK method must create a new array each time it is
99       * called.
100      * </p>
101      *
102      * @return The unmodifiable and sorted list of available locales.
103      */
104     public static List<Locale> availableLocaleList() {
105         return SyncAvoid.AVAILABLE_LOCALE_ULIST;
106     }
107 
108     private static List<Locale> availableLocaleList(final Predicate<Locale> predicate) {
109         return availableLocaleList().stream().filter(predicate).collect(Collectors.toList());
110     }
111 
112     /**
113      * Gets an unmodifiable set of installed locales.
114      *
115      * <p>
116      * This method is a wrapper around {@link Locale#getAvailableLocales()}. It is more efficient, as the JDK method must create a new array each time it is
117      * called.
118      * </p>
119      *
120      * @return The unmodifiable set of available locales.
121      */
122     public static Set<Locale> availableLocaleSet() {
123         return SyncAvoid.AVAILABLE_LOCALE_USET;
124     }
125 
126     /**
127      * Gets the list of countries supported for a given language.
128      *
129      * <p>
130      * This method takes a language code and searches to find the countries available for that language. Variant locales are removed.
131      * </p>
132      *
133      * @param languageCode The 2 letter language code, null returns empty.
134      * @return An unmodifiable List of Locale objects, not null.
135      */
136     public static List<Locale> countriesByLanguage(final String languageCode) {
137         // Only syntactically valid ISO 639 codes can match an available locale's language; anything
138         // else is answered without touching the cache so that arbitrary caller strings are never
139         // retained for the lifetime of the class loader.
140         if (languageCode == null || !languageCode.isEmpty() && !isISO639LanguageCode(languageCode)) {
141             return Collections.emptyList();
142         }
143         return lcToLocalesMap.computeIfAbsent(languageCode, lc -> Collections
144                 .unmodifiableList(availableLocaleList(locale -> languageCode.equals(locale.getLanguage()) && !hasCountry(locale) && hasVariant(locale))));
145     }
146 
147     static ConcurrentMap<String, List<Locale>> getCcToLocalesMap() {
148         return ccToLocalesMap;
149     }
150 
151     /**
152      * Gets the cache of country locales by language.
153      *
154      * @return the cache of country locales by language.
155      */
156     static ConcurrentMap<String, List<Locale>> getLcToLocalesMap() {
157         return lcToLocalesMap;
158     }
159 
160     /**
161      * Tests whether the given Locale defines a variant.
162      *
163      * @param locale The Locale to test.
164      * @return whether the given Locale defines a variant.
165      */
166     private static boolean hasCountry(final Locale locale) {
167         return locale.getCountry().isEmpty();
168     }
169 
170     /**
171      * Tests whether the given Locale defines a country.
172      *
173      * @param locale The Locale to test.
174      * @return whether the given Locale defines a country.
175      */
176     private static boolean hasVariant(final Locale locale) {
177         return locale.getVariant().isEmpty();
178     }
179 
180     /**
181      * Tests whether the given string is the length of an <a href="https://www.iso.org/iso-3166-country-codes.html">ISO 3166</a> alpha-2 country code.
182      *
183      * @param str The string to test.
184      * @return whether the given string is the length of an <a href="https://www.iso.org/iso-3166-country-codes.html">ISO 3166</a> alpha-2 country code.
185      */
186     private static boolean isAlpha2Len(final String str) {
187         return str.length() == 2;
188     }
189 
190     /**
191      * Tests whether the given string is the length of an <a href="https://www.iso.org/iso-3166-country-codes.html">ISO 3166</a> alpha-3 country code.
192      *
193      * @param str The string to test.
194      * @return whether the given string is the length of an <a href="https://www.iso.org/iso-3166-country-codes.html">ISO 3166</a> alpha-3 country code.
195      */
196     private static boolean isAlpha3Len(final String str) {
197         return str.length() == 3;
198     }
199 
200     /**
201      * Tests whether the locale specified is in the set of available locales.
202      *
203      * @param locale The Locale object to check if it is available.
204      * @return true if the locale is a known locale.
205      */
206     public static boolean isAvailableLocale(final Locale locale) {
207         return availableLocaleSet().contains(locale);
208     }
209 
210     /**
211      * Tests whether the given String is a <a href="https://www.iso.org/iso-3166-country-codes.html">ISO 3166</a> alpha-2 country code.
212      *
213      * @param str The String to check.
214      * @return true if the given String is a <a href="https://www.iso.org/iso-3166-country-codes.html">ISO 3166</a> compliant country code.
215      */
216     private static boolean isISO3166CountryCode(final String str) {
217         return StringUtils.isAllUpperCase(str) && isAlpha2Len(str);
218     }
219 
220     /**
221      * Tests whether the given String is a <a href="https://www.iso.org/iso-639-language-code">ISO 639</a> compliant language code.
222      *
223      * @param str The String to check.
224      * @return true, if the given String is a <a href="https://www.iso.org/iso-639-language-code">ISO 639</a> compliant language code.
225      */
226     private static boolean isISO639LanguageCode(final String str) {
227         return StringUtils.isAllLowerCase(str) && (isAlpha2Len(str) || isAlpha3Len(str));
228     }
229 
230     /**
231      * Tests whether a Locale's language is undetermined.
232      * <p>
233      * A Locale's language tag is undetermined if it's value is {@code "und"}. If a language is empty, or not well-formed (for example, "a" or "e2"), it will be
234      * equal to {@code "und"}.
235      * </p>
236      *
237      * @param locale The locale to test.
238      * @return whether a Locale's language is undetermined.
239      * @see Locale#toLanguageTag()
240      * @since 3.14.0
241      */
242     public static boolean isLanguageUndetermined(final Locale locale) {
243         return locale == null || UNDETERMINED.equals(locale.toLanguageTag());
244     }
245 
246     /**
247      * Tests whether the given String is a UN M.49 numeric area code.
248      *
249      * @param str The String to check.
250      * @return true if the given String is a UN M.49 numeric area code.
251      */
252     private static boolean isNumericAreaCode(final String str) {
253         return StringUtils.isNumeric(str) && isAlpha3Len(str);
254     }
255 
256     /**
257      * Obtains the list of languages supported for a given country.
258      *
259      * <p>
260      * This method takes a country code and searches to find the languages available for that country. Variant locales are removed.
261      * </p>
262      *
263      * @param countryCode The 2-letter country code, null returns empty.
264      * @return An unmodifiable List of Locale objects, not null.
265      */
266     public static List<Locale> languagesByCountry(final String countryCode) {
267         // Only syntactically valid ISO 3166 alpha-2 / UN M.49 numeric codes can match an available
268         // locale's country; anything else is answered without touching the cache so that arbitrary
269         // caller strings are never retained for the lifetime of the class loader.
270         if (countryCode == null || !countryCode.isEmpty() && !isISO3166CountryCode(countryCode) && !isNumericAreaCode(countryCode)) {
271             return Collections.emptyList();
272         }
273         return ccToLocalesMap.computeIfAbsent(countryCode,
274                 k -> Collections.unmodifiableList(availableLocaleList(locale -> countryCode.equals(locale.getCountry()) && hasVariant(locale))));
275     }
276 
277     /**
278      * Obtains the list of locales to search through when performing a locale search.
279      *
280      * <pre>
281      * localeLookupList(Locale("fr", "CA", "xxx"))
282      *   = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr")]
283      * </pre>
284      *
285      * @param locale The locale to start from.
286      * @return The unmodifiable list of Locale objects, 0 being locale, not null.
287      */
288     public static List<Locale> localeLookupList(final Locale locale) {
289         return localeLookupList(locale, locale);
290     }
291 
292     /**
293      * Obtains the list of locales to search through when performing a locale search.
294      *
295      * <pre>
296      * localeLookupList(Locale("fr", "CA", "xxx"), Locale("en"))
297      *   = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr"), Locale("en"]
298      * </pre>
299      *
300      * <p>
301      * The result list begins with the most specific locale, then the next more general and so on, finishing with the default locale. The list will never
302      * contain the same locale twice.
303      * </p>
304      *
305      * @param locale        The locale to start from, null returns empty list.
306      * @param defaultLocale The default locale to use if no other is found.
307      * @return The unmodifiable list of Locale objects, 0 being locale, not null.
308      */
309     public static List<Locale> localeLookupList(final Locale locale, final Locale defaultLocale) {
310         final List<Locale> list = new ArrayList<>(4);
311         if (locale != null) {
312             list.add(locale);
313             if (!hasVariant(locale)) {
314                 list.add(new Locale(locale.getLanguage(), locale.getCountry()));
315             }
316             if (!hasCountry(locale)) {
317                 list.add(new Locale(locale.getLanguage(), StringUtils.EMPTY));
318             }
319             if (!list.contains(defaultLocale)) {
320                 list.add(defaultLocale);
321             }
322         }
323         return Collections.unmodifiableList(list);
324     }
325 
326     /**
327      * Creates new {@linkplain Locale} for the given country.
328      *
329      * @param country An ISO 3166 alpha-2 country code or a UN M.49 numeric-3 area code. See the {@linkplain Locale} class description about valid country
330      *                values.
331      * @throws NullPointerException Thrown if either argument is null.
332      * @return A new Locale for the given country.
333      * @see Locale#Locale(String, String)
334      */
335     static Locale ofCountry(final String country) {
336         return new Locale(StringUtils.EMPTY, country);
337     }
338 
339     /**
340      * Tries to parse a Locale from the given String.
341      * <p>
342      * See {@link Locale} for the format.
343      * </p>
344      *
345      * @param str The String to parse as a Locale.
346      * @return A Locale parsed from the given String.
347      * @throws IllegalArgumentException Thrown if the given String cannot be parsed.
348      * @see Locale
349      * @see <a href="https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Locale.html#special_cases_constructor">Locale special cases</a>
350      */
351     private static Locale parseLocale(final String str) {
352         if (isISO639LanguageCode(str)) {
353             return new Locale(str);
354         }
355         final int limit = 3;
356         final char separator = str.indexOf(UNDERSCORE) != -1 ? UNDERSCORE : DASH;
357         final String[] segments = str.split(String.valueOf(separator), 3);
358         final String language = segments[0];
359         if (segments.length == 2) {
360             final String country = segments[1];
361             if (isISO639LanguageCode(language) && (isISO3166CountryCode(country) || isNumericAreaCode(country))) {
362                 return new Locale(language, country);
363             }
364         } else if (segments.length == limit) {
365             final String country = segments[1];
366             final String variant = segments[2];
367             // Special case 1: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Locale.html#special_cases_constructor
368             if (str.equals("th_TH_TH_#u-nu-thai")) {
369                 return new Locale(language, country, "TH");
370             }
371             // Special case 2: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Locale.html#special_cases_constructor
372             if (str.equals("ja_JP_JP_#u-ca-japanese")) {
373                 return new Locale(language, country, "JP");
374             }
375             if (isISO639LanguageCode(language) && (country.isEmpty() || isISO3166CountryCode(country) || isNumericAreaCode(country)) && !variant.isEmpty()) {
376                 return new Locale(language, country, variant);
377             }
378         }
379         if (ArrayUtils.contains(Locale.getISOCountries(), str)) {
380             return new Locale(StringUtils.EMPTY, str);
381         }
382         throw new IllegalArgumentException("Invalid locale format: " + str);
383     }
384 
385     /**
386      * Returns the given locale if non-{@code null}, otherwise {@link Locale#getDefault()}.
387      *
388      * @param locale A locale or {@code null}.
389      * @return The given locale if non-{@code null}, otherwise {@link Locale#getDefault()}.
390      * @since 3.12.0
391      */
392     public static Locale toLocale(final Locale locale) {
393         return locale != null ? locale : Locale.getDefault();
394     }
395 
396     /**
397      * Converts a String to a Locale.
398      *
399      * <p>
400      * This method takes the string format of a locale and creates the locale object from it.
401      * </p>
402      *
403      * <pre>
404      *   LocaleUtils.toLocale("")           = new Locale("", "")
405      *   LocaleUtils.toLocale("en")         = new Locale("en", "")
406      *   LocaleUtils.toLocale("en_GB")      = new Locale("en", "GB")
407      *   LocaleUtils.toLocale("en-GB")      = new Locale("en", "GB")
408      *   LocaleUtils.toLocale("en_001")     = new Locale("en", "001")
409      *   LocaleUtils.toLocale("en_GB_xxx")  = new Locale("en", "GB", "xxx")   (#)
410      *   LocaleUtils.toLocale("US")         = new Locale("", "US") // Because "US" is Locale.getISOCountries()
411      * </pre>
412      *
413      * <p>
414      * (#) The behavior of the JDK variant constructor changed between JDK1.3 and JDK1.4. In JDK1.3, the constructor upper cases the variant, in JDK1.4, it
415      * doesn't. Thus, the result from getVariant() may vary depending on your JDK.
416      * </p>
417      *
418      * <p>
419      * This method validates the input strictly. The language code must be lowercase. The country code must be uppercase. The separator must be an underscore or
420      * a dash. The length must be correct.
421      * </p>
422      *
423      * @param str The locale String to convert, null returns null.
424      * @return A Locale, null if null input.
425      * @throws IllegalArgumentException Thrown if the string is an invalid format.
426      * @see Locale#forLanguageTag(String)
427      * @see Locale#getISOCountries()
428      * @see <a href="https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Locale.html#special_cases_constructor">Locale special cases</a>
429      */
430     public static Locale toLocale(final String str) {
431         if (str == null) {
432             // TODO Should this return the default locale?
433             return null;
434         }
435         if (str.isEmpty()) { // LANG-941 - JDK 8 introduced an empty locale where all fields are blank
436             return new Locale(StringUtils.EMPTY, StringUtils.EMPTY);
437         }
438         final int len = str.length();
439         if (len < 2) {
440             throw new IllegalArgumentException("Invalid locale format: " + str);
441         }
442         final char ch0 = str.charAt(0);
443         if (ch0 == UNDERSCORE || ch0 == DASH) {
444             if (len < 3) {
445                 throw new IllegalArgumentException("Invalid locale format: " + str);
446             }
447             final char ch1 = str.charAt(1);
448             final char ch2 = str.charAt(2);
449             if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) {
450                 throw new IllegalArgumentException("Invalid locale format: " + str);
451             }
452             if (len == 3) {
453                 return new Locale(StringUtils.EMPTY, str.substring(1, 3));
454             }
455             if (len < 5 || str.charAt(3) != ch0) {
456                 throw new IllegalArgumentException("Invalid locale format: " + str);
457             }
458             return new Locale(StringUtils.EMPTY, str.substring(1, 3), str.substring(4));
459         }
460         return parseLocale(str);
461     }
462 
463     /**
464      * {@link LocaleUtils} instances should NOT be constructed in standard programming. Instead, the class should be used as
465      * {@code LocaleUtils.toLocale("en_GB");}.
466      *
467      * <p>
468      * This constructor is public to permit tools that require a JavaBean instance to operate.
469      * </p>
470      *
471      * @deprecated TODO Make private in 4.0.
472      */
473     @Deprecated
474     public LocaleUtils() {
475         // empty
476     }
477 }