Coverage Report - org.apache.commons.lang3.LocaleUtils
 
Classes in this File Line Coverage Branch Coverage Complexity
LocaleUtils
100%
94/94
93%
82/88
5,385
LocaleUtils$SyncAvoid
100%
5/5
N/A
5,385
 
 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  
  *      http://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  
 
 19  
 import java.util.ArrayList;
 20  
 import java.util.Arrays;
 21  
 import java.util.Collections;
 22  
 import java.util.HashSet;
 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  
 
 29  
 /**
 30  
  * <p>Operations to assist when working with a {@link Locale}.</p>
 31  
  *
 32  
  * <p>This class tries to handle {@code null} input gracefully.
 33  
  * An exception will not be thrown for a {@code null} input.
 34  
  * Each method documents its behaviour in more detail.</p>
 35  
  *
 36  
  * @since 2.2
 37  
  */
 38  
 public class LocaleUtils {
 39  
 
 40  
     /** Concurrent map of language locales by country. */
 41  1
     private static final ConcurrentMap<String, List<Locale>> cLanguagesByCountry =
 42  
         new ConcurrentHashMap<>();
 43  
 
 44  
     /** Concurrent map of country locales by language. */
 45  1
     private static final ConcurrentMap<String, List<Locale>> cCountriesByLanguage =
 46  
         new ConcurrentHashMap<>();
 47  
 
 48  
     /**
 49  
      * <p>{@code LocaleUtils} instances should NOT be constructed in standard programming.
 50  
      * Instead, the class should be used as {@code LocaleUtils.toLocale("en_GB");}.</p>
 51  
      *
 52  
      * <p>This constructor is public to permit tools that require a JavaBean instance
 53  
      * to operate.</p>
 54  
      */
 55  
     public LocaleUtils() {
 56  1
       super();
 57  1
     }
 58  
 
 59  
     //-----------------------------------------------------------------------
 60  
     /**
 61  
      * <p>Converts a String to a Locale.</p>
 62  
      *
 63  
      * <p>This method takes the string format of a locale and creates the
 64  
      * locale object from it.</p>
 65  
      *
 66  
      * <pre>
 67  
      *   LocaleUtils.toLocale("")           = new Locale("", "")
 68  
      *   LocaleUtils.toLocale("en")         = new Locale("en", "")
 69  
      *   LocaleUtils.toLocale("en_GB")      = new Locale("en", "GB")
 70  
      *   LocaleUtils.toLocale("en_001")     = new Locale("en", "001")
 71  
      *   LocaleUtils.toLocale("en_GB_xxx")  = new Locale("en", "GB", "xxx")   (#)
 72  
      * </pre>
 73  
      *
 74  
      * <p>(#) The behaviour of the JDK variant constructor changed between JDK1.3 and JDK1.4.
 75  
      * In JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't.
 76  
      * Thus, the result from getVariant() may vary depending on your JDK.</p>
 77  
      *
 78  
      * <p>This method validates the input strictly.
 79  
      * The language code must be lowercase.
 80  
      * The country code must be uppercase.
 81  
      * The separator must be an underscore.
 82  
      * The length must be correct.
 83  
      * </p>
 84  
      *
 85  
      * @param str  the locale String to convert, null returns null
 86  
      * @return a Locale, null if null input
 87  
      * @throws IllegalArgumentException if the string is an invalid format
 88  
      * @see Locale#forLanguageTag(String)
 89  
      */
 90  
     public static Locale toLocale(final String str) {
 91  220
         if (str == null) {
 92  1
             return null;
 93  
         }
 94  219
         if (str.isEmpty()) { // LANG-941 - JDK 8 introduced an empty locale where all fields are blank
 95  2
             return new Locale(StringUtils.EMPTY, StringUtils.EMPTY);
 96  
         }
 97  217
         if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions
 98  3
             throw new IllegalArgumentException("Invalid locale format: " + str);
 99  
         }
 100  214
         final int len = str.length();
 101  214
         if (len < 2) {
 102  1
             throw new IllegalArgumentException("Invalid locale format: " + str);
 103  
         }
 104  213
         final char ch0 = str.charAt(0);
 105  213
         if (ch0 == '_') {
 106  10
             if (len < 3) {
 107  1
                 throw new IllegalArgumentException("Invalid locale format: " + str);
 108  
             }
 109  9
             final char ch1 = str.charAt(1);
 110  9
             final char ch2 = str.charAt(2);
 111  9
             if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) {
 112  4
                 throw new IllegalArgumentException("Invalid locale format: " + str);
 113  
             }
 114  5
             if (len == 3) {
 115  1
                 return new Locale(StringUtils.EMPTY, str.substring(1, 3));
 116  
             }
 117  4
             if (len < 5) {
 118  1
                 throw new IllegalArgumentException("Invalid locale format: " + str);
 119  
             }
 120  3
             if (str.charAt(3) != '_') {
 121  1
                 throw new IllegalArgumentException("Invalid locale format: " + str);
 122  
             }
 123  2
             return new Locale(StringUtils.EMPTY, str.substring(1, 3), str.substring(4));
 124  
         }
 125  
 
 126  203
         return parseLocale(str);
 127  
     }
 128  
 
 129  
     /**
 130  
      * Tries to parse a locale from the given String.
 131  
      *
 132  
      * @param str the String to parse a locale from.
 133  
      * @return a Locale instance parsed from the given String.
 134  
      * @throws IllegalArgumentException if the given String can not be parsed.
 135  
      */
 136  
     private static Locale parseLocale(final String str) {
 137  203
         if (isISO639LanguageCode(str)) {
 138  59
             return new Locale(str);
 139  
         }
 140  
 
 141  144
         final String[] segments = str.split("_", -1);
 142  144
         final String language = segments[0];
 143  144
         if (segments.length == 2) {
 144  129
             final String country = segments[1];
 145  129
             if (isISO639LanguageCode(language) && isISO3166CountryCode(country) ||
 146  10
                     isNumericAreaCode(country)) {
 147  122
                 return new Locale(language, country);
 148  
             }
 149  7
         } else if (segments.length == 3) {
 150  11
             final String country = segments[1];
 151  11
             final String variant = segments[2];
 152  11
             if (isISO639LanguageCode(language) &&
 153  11
                     (country.length() == 0 || isISO3166CountryCode(country) || isNumericAreaCode(country)) &&
 154  11
                     variant.length() > 0) {
 155  10
                 return new Locale(language, country, variant);
 156  
             }
 157  
         }
 158  12
         throw new IllegalArgumentException("Invalid locale format: " + str);
 159  
     }
 160  
 
 161  
     /**
 162  
      * Checks whether the given String is a ISO 639 compliant language code.
 163  
      *
 164  
      * @param str the String to check.
 165  
      * @return true, if the given String is a ISO 639 compliant language code.
 166  
      */
 167  
     private static boolean isISO639LanguageCode(final String str) {
 168  343
         return StringUtils.isAllLowerCase(str) && (str.length() == 2 || str.length() == 3);
 169  
     }
 170  
 
 171  
     /**
 172  
      * Checks whether the given String is a ISO 3166 alpha-2 country code.
 173  
      *
 174  
      * @param str the String to check
 175  
      * @return true, is the given String is a ISO 3166 compliant country code.
 176  
      */
 177  
     private static boolean isISO3166CountryCode(final String str) {
 178  137
         return StringUtils.isAllUpperCase(str) && str.length() == 2;
 179  
     }
 180  
 
 181  
     /**
 182  
      * Checks whether the given String is a UN M.49 numeric area code.
 183  
      *
 184  
      * @param str the String to check
 185  
      * @return true, is the given String is a UN M.49 numeric area code.
 186  
      */
 187  
     private static boolean isNumericAreaCode(final String str) {
 188  12
         return StringUtils.isNumeric(str) && str.length() == 3;
 189  
     }
 190  
 
 191  
     //-----------------------------------------------------------------------
 192  
     /**
 193  
      * <p>Obtains the list of locales to search through when performing
 194  
      * a locale search.</p>
 195  
      *
 196  
      * <pre>
 197  
      * localeLookupList(Locale("fr","CA","xxx"))
 198  
      *   = [Locale("fr","CA","xxx"), Locale("fr","CA"), Locale("fr")]
 199  
      * </pre>
 200  
      *
 201  
      * @param locale  the locale to start from
 202  
      * @return the unmodifiable list of Locale objects, 0 being locale, not null
 203  
      */
 204  
     public static List<Locale> localeLookupList(final Locale locale) {
 205  7
         return localeLookupList(locale, locale);
 206  
     }
 207  
 
 208  
     //-----------------------------------------------------------------------
 209  
     /**
 210  
      * <p>Obtains the list of locales to search through when performing
 211  
      * a locale search.</p>
 212  
      *
 213  
      * <pre>
 214  
      * localeLookupList(Locale("fr", "CA", "xxx"), Locale("en"))
 215  
      *   = [Locale("fr","CA","xxx"), Locale("fr","CA"), Locale("fr"), Locale("en"]
 216  
      * </pre>
 217  
      *
 218  
      * <p>The result list begins with the most specific locale, then the
 219  
      * next more general and so on, finishing with the default locale.
 220  
      * The list will never contain the same locale twice.</p>
 221  
      *
 222  
      * @param locale  the locale to start from, null returns empty list
 223  
      * @param defaultLocale  the default locale to use if no other is found
 224  
      * @return the unmodifiable list of Locale objects, 0 being locale, not null
 225  
      */
 226  
     public static List<Locale> localeLookupList(final Locale locale, final Locale defaultLocale) {
 227  16
         final List<Locale> list = new ArrayList<>(4);
 228  16
         if (locale != null) {
 229  15
             list.add(locale);
 230  15
             if (locale.getVariant().length() > 0) {
 231  5
                 list.add(new Locale(locale.getLanguage(), locale.getCountry()));
 232  
             }
 233  15
             if (locale.getCountry().length() > 0) {
 234  10
                 list.add(new Locale(locale.getLanguage(), StringUtils.EMPTY));
 235  
             }
 236  15
             if (list.contains(defaultLocale) == false) {
 237  5
                 list.add(defaultLocale);
 238  
             }
 239  
         }
 240  16
         return Collections.unmodifiableList(list);
 241  
     }
 242  
 
 243  
     //-----------------------------------------------------------------------
 244  
     /**
 245  
      * <p>Obtains an unmodifiable list of installed locales.</p>
 246  
      *
 247  
      * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
 248  
      * It is more efficient, as the JDK method must create a new array each
 249  
      * time it is called.</p>
 250  
      *
 251  
      * @return the unmodifiable list of available locales
 252  
      */
 253  
     public static List<Locale> availableLocaleList() {
 254  31
         return SyncAvoid.AVAILABLE_LOCALE_LIST;
 255  
     }
 256  
 
 257  
     //-----------------------------------------------------------------------
 258  
     /**
 259  
      * <p>Obtains an unmodifiable set of installed locales.</p>
 260  
      *
 261  
      * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
 262  
      * It is more efficient, as the JDK method must create a new array each
 263  
      * time it is called.</p>
 264  
      *
 265  
      * @return the unmodifiable set of available locales
 266  
      */
 267  
     public static Set<Locale> availableLocaleSet() {
 268  3
         return SyncAvoid.AVAILABLE_LOCALE_SET;
 269  
     }
 270  
 
 271  
     //-----------------------------------------------------------------------
 272  
     /**
 273  
      * <p>Checks if the locale specified is in the list of available locales.</p>
 274  
      *
 275  
      * @param locale the Locale object to check if it is available
 276  
      * @return true if the locale is a known locale
 277  
      */
 278  
     public static boolean isAvailableLocale(final Locale locale) {
 279  23
         return availableLocaleList().contains(locale);
 280  
     }
 281  
 
 282  
     //-----------------------------------------------------------------------
 283  
     /**
 284  
      * <p>Obtains the list of languages supported for a given country.</p>
 285  
      *
 286  
      * <p>This method takes a country code and searches to find the
 287  
      * languages available for that country. Variant locales are removed.</p>
 288  
      *
 289  
      * @param countryCode  the 2 letter country code, null returns empty
 290  
      * @return an unmodifiable List of Locale objects, not null
 291  
      */
 292  
     public static List<Locale> languagesByCountry(final String countryCode) {
 293  8
         if (countryCode == null) {
 294  2
             return Collections.emptyList();
 295  
         }
 296  6
         List<Locale> langs = cLanguagesByCountry.get(countryCode);
 297  6
         if (langs == null) {
 298  3
             langs = new ArrayList<>();
 299  3
             final List<Locale> locales = availableLocaleList();
 300  483
             for (int i = 0; i < locales.size(); i++) {
 301  480
                 final Locale locale = locales.get(i);
 302  480
                 if (countryCode.equals(locale.getCountry()) &&
 303  4
                         locale.getVariant().isEmpty()) {
 304  4
                     langs.add(locale);
 305  
                 }
 306  
             }
 307  3
             langs = Collections.unmodifiableList(langs);
 308  3
             cLanguagesByCountry.putIfAbsent(countryCode, langs);
 309  3
             langs = cLanguagesByCountry.get(countryCode);
 310  
         }
 311  6
         return langs;
 312  
     }
 313  
 
 314  
     //-----------------------------------------------------------------------
 315  
     /**
 316  
      * <p>Obtains the list of countries supported for a given language.</p>
 317  
      *
 318  
      * <p>This method takes a language code and searches to find the
 319  
      * countries available for that language. Variant locales are removed.</p>
 320  
      *
 321  
      * @param languageCode  the 2 letter language code, null returns empty
 322  
      * @return an unmodifiable List of Locale objects, not null
 323  
      */
 324  
     public static List<Locale> countriesByLanguage(final String languageCode) {
 325  8
         if (languageCode == null) {
 326  2
             return Collections.emptyList();
 327  
         }
 328  6
         List<Locale> countries = cCountriesByLanguage.get(languageCode);
 329  6
         if (countries == null) {
 330  3
             countries = new ArrayList<>();
 331  3
             final List<Locale> locales = availableLocaleList();
 332  483
             for (int i = 0; i < locales.size(); i++) {
 333  480
                 final Locale locale = locales.get(i);
 334  480
                 if (languageCode.equals(locale.getLanguage()) &&
 335  9
                         locale.getCountry().length() != 0 &&
 336  7
                         locale.getVariant().isEmpty()) {
 337  7
                     countries.add(locale);
 338  
                 }
 339  
             }
 340  3
             countries = Collections.unmodifiableList(countries);
 341  3
             cCountriesByLanguage.putIfAbsent(languageCode, countries);
 342  3
             countries = cCountriesByLanguage.get(languageCode);
 343  
         }
 344  6
         return countries;
 345  
     }
 346  
 
 347  
     //-----------------------------------------------------------------------
 348  
     // class to avoid synchronization (Init on demand)
 349  34
     static class SyncAvoid {
 350  
         /** Unmodifiable list of available locales. */
 351  
         private static final List<Locale> AVAILABLE_LOCALE_LIST;
 352  
         /** Unmodifiable set of available locales. */
 353  
         private static final Set<Locale> AVAILABLE_LOCALE_SET;
 354  
 
 355  
         static {
 356  1
             final List<Locale> list = new ArrayList<>(Arrays.asList(Locale.getAvailableLocales()));  // extra safe
 357  1
             AVAILABLE_LOCALE_LIST = Collections.unmodifiableList(list);
 358  1
             AVAILABLE_LOCALE_SET = Collections.unmodifiableSet(new HashSet<>(list));
 359  1
         }
 360  
     }
 361  
 
 362  
 }