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