    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    private static final char UNDERSCORE = '_';
    042    private static final char DASH = '-';
    043
    044    // class to avoid synchronization (Init on demand)
    045    static class SyncAvoid {
    046        /** Unmodifiable list of available locales. */
    047        private static final List<Locale> AVAILABLE_LOCALE_LIST;
    048        /** Unmodifiable set of available locales. */
    049        private static final Set<Locale> AVAILABLE_LOCALE_SET;
    050
    051        static {
    052            final List<Locale> list = new ArrayList<>(Arrays.asList(Locale.getAvailableLocales()));  // extra safe
    053            AVAILABLE_LOCALE_LIST = Collections.unmodifiableList(list);
    054            AVAILABLE_LOCALE_SET = Collections.unmodifiableSet(new HashSet<>(list));
    055        }
    056    }
    057
    058    /** Concurrent map of language locales by country. */
    059    private static final ConcurrentMap<String, List<Locale>> cLanguagesByCountry =
    060        new ConcurrentHashMap<>();
    061
    062    /** Concurrent map of country locales by language. */
    063    private static final ConcurrentMap<String, List<Locale>> cCountriesByLanguage =
    064        new ConcurrentHashMap<>();
    065
    066    /**
    067     * Obtains an unmodifiable list of installed locales.
    068     *
    069     * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
    070     * It is more efficient, as the JDK method must create a new array each
    071     * time it is called.</p>
    072     *
    073     * @return the unmodifiable list of available locales
    074     */
    075    public static List<Locale> availableLocaleList() {
    076        return SyncAvoid.AVAILABLE_LOCALE_LIST;
    077    }
    078
    079    private static List<Locale> availableLocaleList(final Predicate<Locale> predicate) {
    080        return availableLocaleList().stream().filter(predicate).collect(Collectors.toList());
    081    }
    082
    083    /**
    084     * Obtains an unmodifiable set of installed locales.
    085     *
    086     * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
    087     * It is more efficient, as the JDK method must create a new array each
    088     * time it is called.</p>
    089     *
    090     * @return the unmodifiable set of available locales
    091     */
    092    public static Set<Locale> availableLocaleSet() {
    093        return SyncAvoid.AVAILABLE_LOCALE_SET;
    094    }
    095
    096    /**
    097     * Obtains the list of countries supported for a given language.
    098     *
    099     * <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     */
    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     */
    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     */
    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     */
    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     */
    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     */
    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     */
    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     */
    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     */
    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     */
    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     */
    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     */
    343    public LocaleUtils() {
    344    }
    345
    346}