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 }