LocaleUtils.java

  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. package org.apache.commons.lang3;

  18. import java.util.ArrayList;
  19. import java.util.Arrays;
  20. import java.util.Collections;
  21. import java.util.Comparator;
  22. import java.util.LinkedHashSet;
  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.  * Operations to assist when working with a {@link Locale}.
  32.  *
  33.  * <p>This class tries to handle {@code null} input gracefully.
  34.  * An exception will not be thrown for a {@code null} input.
  35.  * Each method documents its behavior in more detail.</p>
  36.  *
  37.  * @since 2.2
  38.  */
  39. public class LocaleUtils {

  40.     /**
  41.      * Avoids synchronization, inits on demand.
  42.      */
  43.     private static final class SyncAvoid {

  44.         /** Private unmodifiable and sorted list of available locales. */
  45.         private static final List<Locale> AVAILABLE_LOCALE_ULIST;

  46.         /** Private unmodifiable set of available locales. */
  47.         private static final Set<Locale> AVAILABLE_LOCALE_USET;

  48.         static {
  49.             AVAILABLE_LOCALE_ULIST = Collections
  50.                     .unmodifiableList(Arrays.asList(ArraySorter.sort(Locale.getAvailableLocales(), Comparator.comparing(Locale::toString))));
  51.             AVAILABLE_LOCALE_USET = Collections.unmodifiableSet(new LinkedHashSet<>(AVAILABLE_LOCALE_ULIST));
  52.         }
  53.     }

  54.     /**
  55.      * The underscore character {@code '}{@value}{@code '}.
  56.      */
  57.     private static final char UNDERSCORE = '_';

  58.     /**
  59.      * The undetermined language {@value}.
  60.      * <p>
  61.      * If a language is empty, or not <em>well-formed</am> (for example "a" or "e2"), {@link Locale#toLanguageTag()} will return {@code "und"} (Undetermined).
  62.      * </p>
  63.      *
  64.      * @see Locale#toLanguageTag()
  65.      */
  66.     private static final String UNDETERMINED = "und";

  67.     /**
  68.      * The dash character {@code '}{@value}{@code '}.
  69.      */
  70.     private static final char DASH = '-';

  71.     /**
  72.      * Concurrent map of language locales by country.
  73.      */
  74.     private static final ConcurrentMap<String, List<Locale>> cLanguagesByCountry = new ConcurrentHashMap<>();

  75.     /**
  76.      * Concurrent map of country locales by language.
  77.      */
  78.     private static final ConcurrentMap<String, List<Locale>> cCountriesByLanguage = new ConcurrentHashMap<>();

  79.     /**
  80.      * Obtains an unmodifiable and sorted list of installed locales.
  81.      *
  82.      * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
  83.      * It is more efficient, as the JDK method must create a new array each
  84.      * time it is called.</p>
  85.      *
  86.      * @return the unmodifiable and sorted list of available locales
  87.      */
  88.     public static List<Locale> availableLocaleList() {
  89.         return SyncAvoid.AVAILABLE_LOCALE_ULIST;
  90.     }

  91.     private static List<Locale> availableLocaleList(final Predicate<Locale> predicate) {
  92.         return availableLocaleList().stream().filter(predicate).collect(Collectors.toList());
  93.     }

  94.     /**
  95.      * Obtains an unmodifiable set of installed locales.
  96.      *
  97.      * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
  98.      * It is more efficient, as the JDK method must create a new array each
  99.      * time it is called.</p>
  100.      *
  101.      * @return the unmodifiable set of available locales
  102.      */
  103.     public static Set<Locale> availableLocaleSet() {
  104.         return SyncAvoid.AVAILABLE_LOCALE_USET;
  105.     }

  106.     /**
  107.      * Obtains the list of countries supported for a given language.
  108.      *
  109.      * <p>This method takes a language code and searches to find the
  110.      * countries available for that language. Variant locales are removed.</p>
  111.      *
  112.      * @param languageCode  the 2 letter language code, null returns empty
  113.      * @return an unmodifiable List of Locale objects, not null
  114.      */
  115.     public static List<Locale> countriesByLanguage(final String languageCode) {
  116.         if (languageCode == null) {
  117.             return Collections.emptyList();
  118.         }
  119.         return cCountriesByLanguage.computeIfAbsent(languageCode, lc -> Collections.unmodifiableList(
  120.             availableLocaleList(locale -> languageCode.equals(locale.getLanguage()) && !locale.getCountry().isEmpty() && locale.getVariant().isEmpty())));
  121.     }

  122.     /**
  123.      * Checks if the locale specified is in the set of available locales.
  124.      *
  125.      * @param locale the Locale object to check if it is available
  126.      * @return true if the locale is a known locale
  127.      */
  128.     public static boolean isAvailableLocale(final Locale locale) {
  129.         return availableLocaleSet().contains(locale);
  130.     }

  131.     /**
  132.      * Tests whether the given String is a ISO 3166 alpha-2 country code.
  133.      *
  134.      * @param str the String to check
  135.      * @return true, is the given String is a ISO 3166 compliant country code.
  136.      */
  137.     private static boolean isISO3166CountryCode(final String str) {
  138.         return StringUtils.isAllUpperCase(str) && str.length() == 2;
  139.     }

  140.     /**
  141.      * Tests whether the given String is a ISO 639 compliant language code.
  142.      *
  143.      * @param str the String to check.
  144.      * @return true, if the given String is a ISO 639 compliant language code.
  145.      */
  146.     private static boolean isISO639LanguageCode(final String str) {
  147.         return StringUtils.isAllLowerCase(str) && (str.length() == 2 || str.length() == 3);
  148.     }

  149.     /**
  150.      * Tests whether a Locale's language is undetermined.
  151.      * <p>
  152.      * 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
  153.      * equal to {@code "und"}.
  154.      * </p>
  155.      *
  156.      * @param locale the locale to test.
  157.      * @return whether a Locale's language is undetermined.
  158.      * @see Locale#toLanguageTag()
  159.      * @since 3.14.0
  160.      */
  161.     public static boolean isLanguageUndetermined(final Locale locale) {
  162.         return locale == null || UNDETERMINED.equals(locale.toLanguageTag());
  163.     }

  164.     /**
  165.      * TestsNo whether the given String is a UN M.49 numeric area code.
  166.      *
  167.      * @param str the String to check
  168.      * @return true, is the given String is a UN M.49 numeric area code.
  169.      */
  170.     private static boolean isNumericAreaCode(final String str) {
  171.         return StringUtils.isNumeric(str) && str.length() == 3;
  172.     }

  173.     /**
  174.      * Obtains the list of languages supported for a given country.
  175.      *
  176.      * <p>This method takes a country code and searches to find the
  177.      * languages available for that country. Variant locales are removed.</p>
  178.      *
  179.      * @param countryCode  the 2-letter country code, null returns empty
  180.      * @return an unmodifiable List of Locale objects, not null
  181.      */
  182.     public static List<Locale> languagesByCountry(final String countryCode) {
  183.         if (countryCode == null) {
  184.             return Collections.emptyList();
  185.         }
  186.         return cLanguagesByCountry.computeIfAbsent(countryCode,
  187.             k -> Collections.unmodifiableList(availableLocaleList(locale -> countryCode.equals(locale.getCountry()) && locale.getVariant().isEmpty())));
  188.     }

  189.     /**
  190.      * Obtains the list of locales to search through when performing
  191.      * a locale search.
  192.      *
  193.      * <pre>
  194.      * localeLookupList(Locale("fr", "CA", "xxx"))
  195.      *   = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr")]
  196.      * </pre>
  197.      *
  198.      * @param locale  the locale to start from
  199.      * @return the unmodifiable list of Locale objects, 0 being locale, not null
  200.      */
  201.     public static List<Locale> localeLookupList(final Locale locale) {
  202.         return localeLookupList(locale, locale);
  203.     }

  204.     /**
  205.      * Obtains the list of locales to search through when performing
  206.      * a locale search.
  207.      *
  208.      * <pre>
  209.      * localeLookupList(Locale("fr", "CA", "xxx"), Locale("en"))
  210.      *   = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr"), Locale("en"]
  211.      * </pre>
  212.      *
  213.      * <p>The result list begins with the most specific locale, then the
  214.      * next more general and so on, finishing with the default locale.
  215.      * The list will never contain the same locale twice.</p>
  216.      *
  217.      * @param locale  the locale to start from, null returns empty list
  218.      * @param defaultLocale  the default locale to use if no other is found
  219.      * @return the unmodifiable list of Locale objects, 0 being locale, not null
  220.      */
  221.     public static List<Locale> localeLookupList(final Locale locale, final Locale defaultLocale) {
  222.         final List<Locale> list = new ArrayList<>(4);
  223.         if (locale != null) {
  224.             list.add(locale);
  225.             if (!locale.getVariant().isEmpty()) {
  226.                 list.add(new Locale(locale.getLanguage(), locale.getCountry()));
  227.             }
  228.             if (!locale.getCountry().isEmpty()) {
  229.                 list.add(new Locale(locale.getLanguage(), StringUtils.EMPTY));
  230.             }
  231.             if (!list.contains(defaultLocale)) {
  232.                 list.add(defaultLocale);
  233.             }
  234.         }
  235.         return Collections.unmodifiableList(list);
  236.     }

  237.     /**
  238.      * Tries to parse a Locale from the given String.
  239.      * <p>
  240.      * See {@link Locale} for the format.
  241.      * </p>
  242.      *
  243.      * @param str the String to parse as a Locale.
  244.      * @return a Locale parsed from the given String.
  245.      * @throws IllegalArgumentException if the given String cannot be parsed.
  246.      * @see Locale
  247.      */
  248.     private static Locale parseLocale(final String str) {
  249.         if (isISO639LanguageCode(str)) {
  250.             return new Locale(str);
  251.         }
  252.         final int limit = 3;
  253.         final char separator = str.indexOf(UNDERSCORE) != -1 ? UNDERSCORE : DASH;
  254.         final String[] segments = str.split(String.valueOf(separator), 3);
  255.         final String language = segments[0];
  256.         if (segments.length == 2) {
  257.             final String country = segments[1];
  258.             if (isISO639LanguageCode(language) && isISO3166CountryCode(country) || isNumericAreaCode(country)) {
  259.                 return new Locale(language, country);
  260.             }
  261.         } else if (segments.length == limit) {
  262.             final String country = segments[1];
  263.             final String variant = segments[2];
  264.             if (isISO639LanguageCode(language) &&
  265.                     (country.isEmpty() || isISO3166CountryCode(country) || isNumericAreaCode(country)) &&
  266.                     !variant.isEmpty()) {
  267.                 return new Locale(language, country, variant);
  268.             }
  269.         }
  270.         throw new IllegalArgumentException("Invalid locale format: " + str);
  271.     }

  272.     /**
  273.      * Returns the given locale if non-{@code null}, otherwise {@link Locale#getDefault()}.
  274.      *
  275.      * @param locale a locale or {@code null}.
  276.      * @return the given locale if non-{@code null}, otherwise {@link Locale#getDefault()}.
  277.      * @since 3.12.0
  278.      */
  279.     public static Locale toLocale(final Locale locale) {
  280.         return locale != null ? locale : Locale.getDefault();
  281.     }

  282.     /**
  283.      * Converts a String to a Locale.
  284.      *
  285.      * <p>This method takes the string format of a locale and creates the
  286.      * locale object from it.</p>
  287.      *
  288.      * <pre>
  289.      *   LocaleUtils.toLocale("")           = new Locale("", "")
  290.      *   LocaleUtils.toLocale("en")         = new Locale("en", "")
  291.      *   LocaleUtils.toLocale("en_GB")      = new Locale("en", "GB")
  292.      *   LocaleUtils.toLocale("en-GB")      = new Locale("en", "GB")
  293.      *   LocaleUtils.toLocale("en_001")     = new Locale("en", "001")
  294.      *   LocaleUtils.toLocale("en_GB_xxx")  = new Locale("en", "GB", "xxx")   (#)
  295.      * </pre>
  296.      *
  297.      * <p>(#) The behavior of the JDK variant constructor changed between JDK1.3 and JDK1.4.
  298.      * In JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't.
  299.      * Thus, the result from getVariant() may vary depending on your JDK.</p>
  300.      *
  301.      * <p>This method validates the input strictly.
  302.      * The language code must be lowercase.
  303.      * The country code must be uppercase.
  304.      * The separator must be an underscore or a dash.
  305.      * The length must be correct.
  306.      * </p>
  307.      *
  308.      * @param str  the locale String to convert, null returns null
  309.      * @return a Locale, null if null input
  310.      * @throws IllegalArgumentException if the string is an invalid format
  311.      * @see Locale#forLanguageTag(String)
  312.      */
  313.     public static Locale toLocale(final String str) {
  314.         if (str == null) {
  315.             // TODO Should this return the default locale?
  316.             return null;
  317.         }
  318.         if (str.isEmpty()) { // LANG-941 - JDK 8 introduced an empty locale where all fields are blank
  319.             return new Locale(StringUtils.EMPTY, StringUtils.EMPTY);
  320.         }
  321.         if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions
  322.             throw new IllegalArgumentException("Invalid locale format: " + str);
  323.         }
  324.         final int len = str.length();
  325.         if (len < 2) {
  326.             throw new IllegalArgumentException("Invalid locale format: " + str);
  327.         }
  328.         final char ch0 = str.charAt(0);
  329.         if (ch0 == UNDERSCORE || ch0 == DASH) {
  330.             if (len < 3) {
  331.                 throw new IllegalArgumentException("Invalid locale format: " + str);
  332.             }
  333.             final char ch1 = str.charAt(1);
  334.             final char ch2 = str.charAt(2);
  335.             if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) {
  336.                 throw new IllegalArgumentException("Invalid locale format: " + str);
  337.             }
  338.             if (len == 3) {
  339.                 return new Locale(StringUtils.EMPTY, str.substring(1, 3));
  340.             }
  341.             if (len < 5) {
  342.                 throw new IllegalArgumentException("Invalid locale format: " + str);
  343.             }
  344.             if (str.charAt(3) != ch0) {
  345.                 throw new IllegalArgumentException("Invalid locale format: " + str);
  346.             }
  347.             return new Locale(StringUtils.EMPTY, str.substring(1, 3), str.substring(4));
  348.         }

  349.         return parseLocale(str);
  350.     }

  351.     /**
  352.      * {@link LocaleUtils} instances should NOT be constructed in standard programming.
  353.      * Instead, the class should be used as {@code LocaleUtils.toLocale("en_GB");}.
  354.      *
  355.      * <p>This constructor is public to permit tools that require a JavaBean instance
  356.      * to operate.</p>
  357.      *
  358.      * @deprecated TODO Make private in 4.0.
  359.      */
  360.     @Deprecated
  361.     public LocaleUtils() {
  362.         // empty
  363.     }

  364. }