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; 028 029/** 030 * <p>Operations to assist when working with a {@link Locale}.</p> 031 * 032 * <p>This class tries to handle {@code null} input gracefully. 033 * An exception will not be thrown for a {@code null} input. 034 * Each method documents its behaviour in more detail.</p> 035 * 036 * @since 2.2 037 */ 038public class LocaleUtils { 039 040 /** Concurrent map of language locales by country. */ 041 private static final ConcurrentMap<String, List<Locale>> cLanguagesByCountry = 042 new ConcurrentHashMap<>(); 043 044 /** Concurrent map of country locales by language. */ 045 private static final ConcurrentMap<String, List<Locale>> cCountriesByLanguage = 046 new ConcurrentHashMap<>(); 047 048 /** 049 * <p>{@code LocaleUtils} instances should NOT be constructed in standard programming. 050 * Instead, the class should be used as {@code LocaleUtils.toLocale("en_GB");}.</p> 051 * 052 * <p>This constructor is public to permit tools that require a JavaBean instance 053 * to operate.</p> 054 */ 055 public LocaleUtils() { 056 super(); 057 } 058 059 //----------------------------------------------------------------------- 060 /** 061 * <p>Converts a String to a Locale.</p> 062 * 063 * <p>This method takes the string format of a locale and creates the 064 * locale object from it.</p> 065 * 066 * <pre> 067 * LocaleUtils.toLocale("") = new Locale("", "") 068 * LocaleUtils.toLocale("en") = new Locale("en", "") 069 * LocaleUtils.toLocale("en_GB") = new Locale("en", "GB") 070 * LocaleUtils.toLocale("en_001") = new Locale("en", "001") 071 * LocaleUtils.toLocale("en_GB_xxx") = new Locale("en", "GB", "xxx") (#) 072 * </pre> 073 * 074 * <p>(#) The behaviour of the JDK variant constructor changed between JDK1.3 and JDK1.4. 075 * In JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't. 076 * Thus, the result from getVariant() may vary depending on your JDK.</p> 077 * 078 * <p>This method validates the input strictly. 079 * The language code must be lowercase. 080 * The country code must be uppercase. 081 * The separator must be an underscore. 082 * The length must be correct. 083 * </p> 084 * 085 * @param str the locale String to convert, null returns null 086 * @return a Locale, null if null input 087 * @throws IllegalArgumentException if the string is an invalid format 088 * @see Locale#forLanguageTag(String) 089 */ 090 public static Locale toLocale(final String str) { 091 if (str == null) { 092 return null; 093 } 094 if (str.isEmpty()) { // LANG-941 - JDK 8 introduced an empty locale where all fields are blank 095 return new Locale(StringUtils.EMPTY, StringUtils.EMPTY); 096 } 097 if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions 098 throw new IllegalArgumentException("Invalid locale format: " + str); 099 } 100 final int len = str.length(); 101 if (len < 2) { 102 throw new IllegalArgumentException("Invalid locale format: " + str); 103 } 104 final char ch0 = str.charAt(0); 105 if (ch0 == '_') { 106 if (len < 3) { 107 throw new IllegalArgumentException("Invalid locale format: " + str); 108 } 109 final char ch1 = str.charAt(1); 110 final char ch2 = str.charAt(2); 111 if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) { 112 throw new IllegalArgumentException("Invalid locale format: " + str); 113 } 114 if (len == 3) { 115 return new Locale(StringUtils.EMPTY, str.substring(1, 3)); 116 } 117 if (len < 5) { 118 throw new IllegalArgumentException("Invalid locale format: " + str); 119 } 120 if (str.charAt(3) != '_') { 121 throw new IllegalArgumentException("Invalid locale format: " + str); 122 } 123 return new Locale(StringUtils.EMPTY, str.substring(1, 3), str.substring(4)); 124 } 125 126 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 if (isISO639LanguageCode(str)) { 138 return new Locale(str); 139 } 140 141 final String[] segments = str.split("_", -1); 142 final String language = segments[0]; 143 if (segments.length == 2) { 144 final String country = segments[1]; 145 if (isISO639LanguageCode(language) && isISO3166CountryCode(country) || 146 isNumericAreaCode(country)) { 147 return new Locale(language, country); 148 } 149 } else if (segments.length == 3) { 150 final String country = segments[1]; 151 final String variant = segments[2]; 152 if (isISO639LanguageCode(language) && 153 (country.isEmpty() || isISO3166CountryCode(country) || isNumericAreaCode(country)) && 154 !variant.isEmpty()) { 155 return new Locale(language, country, variant); 156 } 157 } 158 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 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 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 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 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 final List<Locale> list = new ArrayList<>(4); 228 if (locale != null) { 229 list.add(locale); 230 if (!locale.getVariant().isEmpty()) { 231 list.add(new Locale(locale.getLanguage(), locale.getCountry())); 232 } 233 if (!locale.getCountry().isEmpty()) { 234 list.add(new Locale(locale.getLanguage(), StringUtils.EMPTY)); 235 } 236 if (!list.contains(defaultLocale)) { 237 list.add(defaultLocale); 238 } 239 } 240 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 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 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 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 if (countryCode == null) { 294 return Collections.emptyList(); 295 } 296 List<Locale> langs = cLanguagesByCountry.get(countryCode); 297 if (langs == null) { 298 langs = new ArrayList<>(); 299 final List<Locale> locales = availableLocaleList(); 300 for (final Locale locale : locales) { 301 if (countryCode.equals(locale.getCountry()) && 302 locale.getVariant().isEmpty()) { 303 langs.add(locale); 304 } 305 } 306 langs = Collections.unmodifiableList(langs); 307 cLanguagesByCountry.putIfAbsent(countryCode, langs); 308 langs = cLanguagesByCountry.get(countryCode); 309 } 310 return langs; 311 } 312 313 //----------------------------------------------------------------------- 314 /** 315 * <p>Obtains the list of countries supported for a given language.</p> 316 * 317 * <p>This method takes a language code and searches to find the 318 * countries available for that language. Variant locales are removed.</p> 319 * 320 * @param languageCode the 2 letter language code, null returns empty 321 * @return an unmodifiable List of Locale objects, not null 322 */ 323 public static List<Locale> countriesByLanguage(final String languageCode) { 324 if (languageCode == null) { 325 return Collections.emptyList(); 326 } 327 List<Locale> countries = cCountriesByLanguage.get(languageCode); 328 if (countries == null) { 329 countries = new ArrayList<>(); 330 final List<Locale> locales = availableLocaleList(); 331 for (final Locale locale : locales) { 332 if (languageCode.equals(locale.getLanguage()) && 333 !locale.getCountry().isEmpty() && 334 locale.getVariant().isEmpty()) { 335 countries.add(locale); 336 } 337 } 338 countries = Collections.unmodifiableList(countries); 339 cCountriesByLanguage.putIfAbsent(languageCode, countries); 340 countries = cCountriesByLanguage.get(languageCode); 341 } 342 return countries; 343 } 344 345 //----------------------------------------------------------------------- 346 // class to avoid synchronization (Init on demand) 347 static class SyncAvoid { 348 /** Unmodifiable list of available locales. */ 349 private static final List<Locale> AVAILABLE_LOCALE_LIST; 350 /** Unmodifiable set of available locales. */ 351 private static final Set<Locale> AVAILABLE_LOCALE_SET; 352 353 static { 354 final List<Locale> list = new ArrayList<>(Arrays.asList(Locale.getAvailableLocales())); // extra safe 355 AVAILABLE_LOCALE_LIST = Collections.unmodifiableList(list); 356 AVAILABLE_LOCALE_SET = Collections.unmodifiableSet(new HashSet<>(list)); 357 } 358 } 359 360}