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 */
017package org.apache.commons.lang3;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Locale;
025import java.util.Set;
026import java.util.concurrent.ConcurrentHashMap;
027import java.util.concurrent.ConcurrentMap;
028import java.util.function.Predicate;
029import java.util.stream.Collectors;
030
031/**
032 * Operations to assist when working with a {@link Locale}.
033 *
034 * <p>This class tries to handle {@code null} input gracefully.
035 * An exception will not be thrown for a {@code null} input.
036 * Each method documents its behavior in more detail.</p>
037 *
038 * @since 2.2
039 */
040public class LocaleUtils {
041
042    // class to avoid synchronization (Init on demand)
043    static class SyncAvoid {
044        /** Unmodifiable list of available locales. */
045        private static final List<Locale> AVAILABLE_LOCALE_LIST;
046        /** Unmodifiable set of available locales. */
047        private static final Set<Locale> AVAILABLE_LOCALE_SET;
048
049        static {
050            final List<Locale> list = new ArrayList<>(Arrays.asList(Locale.getAvailableLocales()));  // extra safe
051            AVAILABLE_LOCALE_LIST = Collections.unmodifiableList(list);
052            AVAILABLE_LOCALE_SET = Collections.unmodifiableSet(new HashSet<>(list));
053        }
054    }
055
056    /**
057     * The underscore character {@code '}{@value}{@code '}.
058     */
059    private static final char UNDERSCORE = '_';
060
061    /**
062     * The undetermined language {@value}.
063     */
064    private static final String UNDETERMINED = "und";
065
066    /**
067     * The dash character {@code '}{@value}{@code '}.
068     */
069    private static final char DASH = '-';
070
071    /**
072     * Concurrent map of language locales by country.
073     */
074    private static final ConcurrentMap<String, List<Locale>> cLanguagesByCountry = new ConcurrentHashMap<>();
075
076    /**
077     * Concurrent map of country locales by language.
078     */
079    private static final ConcurrentMap<String, List<Locale>> cCountriesByLanguage = new ConcurrentHashMap<>();
080
081    /**
082     * Obtains an unmodifiable list of installed locales.
083     *
084     * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
085     * It is more efficient, as the JDK method must create a new array each
086     * time it is called.</p>
087     *
088     * @return the unmodifiable list of available locales
089     */
090    public static List<Locale> availableLocaleList() {
091        return SyncAvoid.AVAILABLE_LOCALE_LIST;
092    }
093
094    private static List<Locale> availableLocaleList(final Predicate<Locale> predicate) {
095        return availableLocaleList().stream().filter(predicate).collect(Collectors.toList());
096    }
097
098    /**
099     * Obtains an unmodifiable set of installed locales.
100     *
101     * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
102     * It is more efficient, as the JDK method must create a new array each
103     * time it is called.</p>
104     *
105     * @return the unmodifiable set of available locales
106     */
107    public static Set<Locale> availableLocaleSet() {
108        return SyncAvoid.AVAILABLE_LOCALE_SET;
109    }
110
111    /**
112     * Obtains the list of countries supported for a given language.
113     *
114     * <p>This method takes a language code and searches to find the
115     * countries available for that language. Variant locales are removed.</p>
116     *
117     * @param languageCode  the 2 letter language code, null returns empty
118     * @return an unmodifiable List of Locale objects, not null
119     */
120    public static List<Locale> countriesByLanguage(final String languageCode) {
121        if (languageCode == null) {
122            return Collections.emptyList();
123        }
124        return cCountriesByLanguage.computeIfAbsent(languageCode, lc -> Collections.unmodifiableList(
125            availableLocaleList(locale -> languageCode.equals(locale.getLanguage()) && !locale.getCountry().isEmpty() && locale.getVariant().isEmpty())));
126    }
127
128    /**
129     * Checks if the locale specified is in the set of available locales.
130     *
131     * @param locale the Locale object to check if it is available
132     * @return true if the locale is a known locale
133     */
134    public static boolean isAvailableLocale(final Locale locale) {
135        return availableLocaleSet().contains(locale);
136    }
137
138    /**
139     * Checks whether the given String is a ISO 3166 alpha-2 country code.
140     *
141     * @param str the String to check
142     * @return true, is the given String is a ISO 3166 compliant country code.
143     */
144    private static boolean isISO3166CountryCode(final String str) {
145        return StringUtils.isAllUpperCase(str) && str.length() == 2;
146    }
147
148    /**
149     * Checks whether the given String is a ISO 639 compliant language code.
150     *
151     * @param str the String to check.
152     * @return true, if the given String is a ISO 639 compliant language code.
153     */
154    private static boolean isISO639LanguageCode(final String str) {
155        return StringUtils.isAllLowerCase(str) && (str.length() == 2 || str.length() == 3);
156    }
157
158    /**
159     * Tests whether a Locale's language is undetermined.
160     * <p>
161     * 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
162     * equal to {@code "und"}.
163     * </p>
164     *
165     * @param locale the locale to test.
166     * @return whether a Locale's language is undetermined.
167     * @see Locale#toLanguageTag()
168     * @since 3.14.0
169     */
170    public static boolean isLanguageUndetermined(final Locale locale) {
171        return locale == null || UNDETERMINED.equals(locale.toLanguageTag());
172    }
173
174    /**
175     * Checks whether the given String is a UN M.49 numeric area code.
176     *
177     * @param str the String to check
178     * @return true, is the given String is a UN M.49 numeric area code.
179     */
180    private static boolean isNumericAreaCode(final String str) {
181        return StringUtils.isNumeric(str) && str.length() == 3;
182    }
183
184    /**
185     * Obtains the list of languages supported for a given country.
186     *
187     * <p>This method takes a country code and searches to find the
188     * languages available for that country. Variant locales are removed.</p>
189     *
190     * @param countryCode  the 2-letter country code, null returns empty
191     * @return an unmodifiable List of Locale objects, not null
192     */
193    public static List<Locale> languagesByCountry(final String countryCode) {
194        if (countryCode == null) {
195            return Collections.emptyList();
196        }
197        return cLanguagesByCountry.computeIfAbsent(countryCode,
198            k -> Collections.unmodifiableList(availableLocaleList(locale -> countryCode.equals(locale.getCountry()) && locale.getVariant().isEmpty())));
199    }
200
201    /**
202     * Obtains the list of locales to search through when performing
203     * a locale search.
204     *
205     * <pre>
206     * localeLookupList(Locale("fr", "CA", "xxx"))
207     *   = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr")]
208     * </pre>
209     *
210     * @param locale  the locale to start from
211     * @return the unmodifiable list of Locale objects, 0 being locale, not null
212     */
213    public static List<Locale> localeLookupList(final Locale locale) {
214        return localeLookupList(locale, locale);
215    }
216
217    /**
218     * Obtains the list of locales to search through when performing
219     * a locale search.
220     *
221     * <pre>
222     * localeLookupList(Locale("fr", "CA", "xxx"), Locale("en"))
223     *   = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr"), Locale("en"]
224     * </pre>
225     *
226     * <p>The result list begins with the most specific locale, then the
227     * next more general and so on, finishing with the default locale.
228     * The list will never contain the same locale twice.</p>
229     *
230     * @param locale  the locale to start from, null returns empty list
231     * @param defaultLocale  the default locale to use if no other is found
232     * @return the unmodifiable list of Locale objects, 0 being locale, not null
233     */
234    public static List<Locale> localeLookupList(final Locale locale, final Locale defaultLocale) {
235        final List<Locale> list = new ArrayList<>(4);
236        if (locale != null) {
237            list.add(locale);
238            if (!locale.getVariant().isEmpty()) {
239                list.add(new Locale(locale.getLanguage(), locale.getCountry()));
240            }
241            if (!locale.getCountry().isEmpty()) {
242                list.add(new Locale(locale.getLanguage(), StringUtils.EMPTY));
243            }
244            if (!list.contains(defaultLocale)) {
245                list.add(defaultLocale);
246            }
247        }
248        return Collections.unmodifiableList(list);
249    }
250
251    /**
252     * Tries to parse a Locale from the given String.
253     * <p>
254     * See {@Link Locale} for the format.
255     * </p>
256     *
257     * @param str the String to parse as a Locale.
258     * @return a Locale parsed from the given String.
259     * @throws IllegalArgumentException if the given String can not be parsed.
260     * @see Locale
261     */
262    private static Locale parseLocale(final String str) {
263        if (isISO639LanguageCode(str)) {
264            return new Locale(str);
265        }
266        final int limit = 3;
267        final char separator = str.indexOf(UNDERSCORE) != -1 ? UNDERSCORE : DASH;
268        final String[] segments = str.split(String.valueOf(separator), 3);
269        final String language = segments[0];
270        if (segments.length == 2) {
271            final String country = segments[1];
272            if (isISO639LanguageCode(language) && isISO3166CountryCode(country) || isNumericAreaCode(country)) {
273                return new Locale(language, country);
274            }
275        } else if (segments.length == limit) {
276            final String country = segments[1];
277            final String variant = segments[2];
278            if (isISO639LanguageCode(language) &&
279                    (country.isEmpty() || isISO3166CountryCode(country) || isNumericAreaCode(country)) &&
280                    !variant.isEmpty()) {
281                return new Locale(language, country, variant);
282            }
283        }
284        throw new IllegalArgumentException("Invalid locale format: " + str);
285    }
286
287    /**
288     * Returns the given locale if non-{@code null}, otherwise {@link Locale#getDefault()}.
289     *
290     * @param locale a locale or {@code null}.
291     * @return the given locale if non-{@code null}, otherwise {@link Locale#getDefault()}.
292     * @since 3.12.0
293     */
294    public static Locale toLocale(final Locale locale) {
295        return locale != null ? locale : Locale.getDefault();
296    }
297
298    /**
299     * Converts a String to a Locale.
300     *
301     * <p>This method takes the string format of a locale and creates the
302     * locale object from it.</p>
303     *
304     * <pre>
305     *   LocaleUtils.toLocale("")           = new Locale("", "")
306     *   LocaleUtils.toLocale("en")         = new Locale("en", "")
307     *   LocaleUtils.toLocale("en_GB")      = new Locale("en", "GB")
308     *   LocaleUtils.toLocale("en-GB")      = new Locale("en", "GB")
309     *   LocaleUtils.toLocale("en_001")     = new Locale("en", "001")
310     *   LocaleUtils.toLocale("en_GB_xxx")  = new Locale("en", "GB", "xxx")   (#)
311     * </pre>
312     *
313     * <p>(#) The behavior of the JDK variant constructor changed between JDK1.3 and JDK1.4.
314     * In JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't.
315     * Thus, the result from getVariant() may vary depending on your JDK.</p>
316     *
317     * <p>This method validates the input strictly.
318     * The language code must be lowercase.
319     * The country code must be uppercase.
320     * The separator must be an underscore or a dash.
321     * The length must be correct.
322     * </p>
323     *
324     * @param str  the locale String to convert, null returns null
325     * @return a Locale, null if null input
326     * @throws IllegalArgumentException if the string is an invalid format
327     * @see Locale#forLanguageTag(String)
328     */
329    public static Locale toLocale(final String str) {
330        if (str == null) {
331            // TODO Should this return the default locale?
332            return null;
333        }
334        if (str.isEmpty()) { // LANG-941 - JDK 8 introduced an empty locale where all fields are blank
335            return new Locale(StringUtils.EMPTY, StringUtils.EMPTY);
336        }
337        if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions
338            throw new IllegalArgumentException("Invalid locale format: " + str);
339        }
340        final int len = str.length();
341        if (len < 2) {
342            throw new IllegalArgumentException("Invalid locale format: " + str);
343        }
344        final char ch0 = str.charAt(0);
345        if (ch0 == UNDERSCORE || ch0 == DASH) {
346            if (len < 3) {
347                throw new IllegalArgumentException("Invalid locale format: " + str);
348            }
349            final char ch1 = str.charAt(1);
350            final char ch2 = str.charAt(2);
351            if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) {
352                throw new IllegalArgumentException("Invalid locale format: " + str);
353            }
354            if (len == 3) {
355                return new Locale(StringUtils.EMPTY, str.substring(1, 3));
356            }
357            if (len < 5) {
358                throw new IllegalArgumentException("Invalid locale format: " + str);
359            }
360            if (str.charAt(3) != ch0) {
361                throw new IllegalArgumentException("Invalid locale format: " + str);
362            }
363            return new Locale(StringUtils.EMPTY, str.substring(1, 3), str.substring(4));
364        }
365
366        return parseLocale(str);
367    }
368
369    /**
370     * {@link LocaleUtils} instances should NOT be constructed in standard programming.
371     * Instead, the class should be used as {@code LocaleUtils.toLocale("en_GB");}.
372     *
373     * <p>This constructor is public to permit tools that require a JavaBean instance
374     * to operate.</p>
375     *
376     * @deprecated TODO Make private in 4.0.
377     */
378    @Deprecated
379    public LocaleUtils() {
380        // empty
381    }
382
383}