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