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; 028import java.util.function.Predicate; 029import java.util.stream.Collectors; 030 031/** 032 * Operations to assist when working with a {@link Locale}. 033 * 034 * <p>This class tries to handle {@code null} input gracefully. 035 * An exception will not be thrown for a {@code null} input. 036 * Each method documents its behavior in more detail.</p> 037 * 038 * @since 2.2 039 */ 040public class LocaleUtils { 041 042 // class to avoid synchronization (Init on demand) 043 static class SyncAvoid { 044 /** Unmodifiable list of available locales. */ 045 private static final List<Locale> AVAILABLE_LOCALE_LIST; 046 /** Unmodifiable set of available locales. */ 047 private static final Set<Locale> AVAILABLE_LOCALE_SET; 048 049 static { 050 final List<Locale> list = new ArrayList<>(Arrays.asList(Locale.getAvailableLocales())); // extra safe 051 AVAILABLE_LOCALE_LIST = Collections.unmodifiableList(list); 052 AVAILABLE_LOCALE_SET = Collections.unmodifiableSet(new HashSet<>(list)); 053 } 054 } 055 private static final char UNDERSCORE = '_'; 056 private static final String UNDETERMINED = "und"; 057 058 private static final char DASH = '-'; 059 060 /** Concurrent map of language locales by country. */ 061 private static final ConcurrentMap<String, List<Locale>> cLanguagesByCountry = 062 new ConcurrentHashMap<>(); 063 064 /** Concurrent map of country locales by language. */ 065 private static final ConcurrentMap<String, List<Locale>> cCountriesByLanguage = 066 new ConcurrentHashMap<>(); 067 068 /** 069 * Obtains an unmodifiable list of installed locales. 070 * 071 * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}. 072 * It is more efficient, as the JDK method must create a new array each 073 * time it is called.</p> 074 * 075 * @return the unmodifiable list of available locales 076 */ 077 public static List<Locale> availableLocaleList() { 078 return SyncAvoid.AVAILABLE_LOCALE_LIST; 079 } 080 081 private static List<Locale> availableLocaleList(final Predicate<Locale> predicate) { 082 return availableLocaleList().stream().filter(predicate).collect(Collectors.toList()); 083 } 084 085 /** 086 * Obtains an unmodifiable set of installed locales. 087 * 088 * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}. 089 * It is more efficient, as the JDK method must create a new array each 090 * time it is called.</p> 091 * 092 * @return the unmodifiable set of available locales 093 */ 094 public static Set<Locale> availableLocaleSet() { 095 return SyncAvoid.AVAILABLE_LOCALE_SET; 096 } 097 098 /** 099 * Obtains the list of countries supported for a given language. 100 * 101 * <p>This method takes a language code and searches to find the 102 * countries available for that language. Variant locales are removed.</p> 103 * 104 * @param languageCode the 2 letter language code, null returns empty 105 * @return an unmodifiable List of Locale objects, not null 106 */ 107 public static List<Locale> countriesByLanguage(final String languageCode) { 108 if (languageCode == null) { 109 return Collections.emptyList(); 110 } 111 return cCountriesByLanguage.computeIfAbsent(languageCode, lc -> Collections.unmodifiableList( 112 availableLocaleList(locale -> languageCode.equals(locale.getLanguage()) && !locale.getCountry().isEmpty() && locale.getVariant().isEmpty()))); 113 } 114 115 /** 116 * Checks if the locale specified is in the set of available locales. 117 * 118 * @param locale the Locale object to check if it is available 119 * @return true if the locale is a known locale 120 */ 121 public static boolean isAvailableLocale(final Locale locale) { 122 return availableLocaleSet().contains(locale); 123 } 124 125 /** 126 * Checks whether the given String is a ISO 3166 alpha-2 country code. 127 * 128 * @param str the String to check 129 * @return true, is the given String is a ISO 3166 compliant country code. 130 */ 131 private static boolean isISO3166CountryCode(final String str) { 132 return StringUtils.isAllUpperCase(str) && str.length() == 2; 133 } 134 135 /** 136 * Checks whether the given String is a ISO 639 compliant language code. 137 * 138 * @param str the String to check. 139 * @return true, if the given String is a ISO 639 compliant language code. 140 */ 141 private static boolean isISO639LanguageCode(final String str) { 142 return StringUtils.isAllLowerCase(str) && (str.length() == 2 || str.length() == 3); 143 } 144 145 /** 146 * Tests whether a Locale's language is undetermined. 147 * <p> 148 * 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 149 * equal to {@code "und"}. 150 * </p> 151 * 152 * @param locale the locale to test. 153 * @return whether a Locale's language is undetermined. 154 * @see Locale#toLanguageTag() 155 * @since 3.14.0 156 */ 157 public static boolean isLanguageUndetermined(final Locale locale) { 158 return locale == null || UNDETERMINED.equals(locale.toLanguageTag()); 159 } 160 161 /** 162 * Checks whether the given String is a UN M.49 numeric area code. 163 * 164 * @param str the String to check 165 * @return true, is the given String is a UN M.49 numeric area code. 166 */ 167 private static boolean isNumericAreaCode(final String str) { 168 return StringUtils.isNumeric(str) && str.length() == 3; 169 } 170 171 /** 172 * Obtains the list of languages supported for a given country. 173 * 174 * <p>This method takes a country code and searches to find the 175 * languages available for that country. Variant locales are removed.</p> 176 * 177 * @param countryCode the 2-letter country code, null returns empty 178 * @return an unmodifiable List of Locale objects, not null 179 */ 180 public static List<Locale> languagesByCountry(final String countryCode) { 181 if (countryCode == null) { 182 return Collections.emptyList(); 183 } 184 return cLanguagesByCountry.computeIfAbsent(countryCode, 185 k -> Collections.unmodifiableList(availableLocaleList(locale -> countryCode.equals(locale.getCountry()) && locale.getVariant().isEmpty()))); 186 } 187 188 /** 189 * Obtains the list of locales to search through when performing 190 * a locale search. 191 * 192 * <pre> 193 * localeLookupList(Locale("fr", "CA", "xxx")) 194 * = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr")] 195 * </pre> 196 * 197 * @param locale the locale to start from 198 * @return the unmodifiable list of Locale objects, 0 being locale, not null 199 */ 200 public static List<Locale> localeLookupList(final Locale locale) { 201 return localeLookupList(locale, locale); 202 } 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 /** 239 * Tries to parse a locale from the given String. 240 * 241 * @param str the String to parse a locale from. 242 * @return a Locale instance parsed from the given String. 243 * @throws IllegalArgumentException if the given String can not be parsed. 244 */ 245 private static Locale parseLocale(final String str) { 246 if (isISO639LanguageCode(str)) { 247 return new Locale(str); 248 } 249 250 final String[] segments = str.indexOf(UNDERSCORE) != -1 251 ? str.split(String.valueOf(UNDERSCORE), -1) 252 : str.split(String.valueOf(DASH), -1); 253 final String language = segments[0]; 254 if (segments.length == 2) { 255 final String country = segments[1]; 256 if (isISO639LanguageCode(language) && isISO3166CountryCode(country) || 257 isNumericAreaCode(country)) { 258 return new Locale(language, country); 259 } 260 } else if (segments.length == 3) { 261 final String country = segments[1]; 262 final String variant = segments[2]; 263 if (isISO639LanguageCode(language) && 264 (country.isEmpty() || isISO3166CountryCode(country) || isNumericAreaCode(country)) && 265 !variant.isEmpty()) { 266 return new Locale(language, country, variant); 267 } 268 } 269 throw new IllegalArgumentException("Invalid locale format: " + str); 270 } 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 /** 284 * Converts a String to a Locale. 285 * 286 * <p>This method takes the string format of a locale and creates the 287 * locale object from it.</p> 288 * 289 * <pre> 290 * LocaleUtils.toLocale("") = new Locale("", "") 291 * LocaleUtils.toLocale("en") = new Locale("en", "") 292 * LocaleUtils.toLocale("en_GB") = new Locale("en", "GB") 293 * LocaleUtils.toLocale("en-GB") = new Locale("en", "GB") 294 * LocaleUtils.toLocale("en_001") = new Locale("en", "001") 295 * LocaleUtils.toLocale("en_GB_xxx") = new Locale("en", "GB", "xxx") (#) 296 * </pre> 297 * 298 * <p>(#) The behavior of the JDK variant constructor changed between JDK1.3 and JDK1.4. 299 * In JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't. 300 * Thus, the result from getVariant() may vary depending on your JDK.</p> 301 * 302 * <p>This method validates the input strictly. 303 * The language code must be lowercase. 304 * The country code must be uppercase. 305 * The separator must be an underscore or a dash. 306 * The length must be correct. 307 * </p> 308 * 309 * @param str the locale String to convert, null returns null 310 * @return a Locale, null if null input 311 * @throws IllegalArgumentException if the string is an invalid format 312 * @see Locale#forLanguageTag(String) 313 */ 314 public static Locale toLocale(final String str) { 315 if (str == null) { 316 // TODO Should this return the default locale? 317 return null; 318 } 319 if (str.isEmpty()) { // LANG-941 - JDK 8 introduced an empty locale where all fields are blank 320 return new Locale(StringUtils.EMPTY, StringUtils.EMPTY); 321 } 322 if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions 323 throw new IllegalArgumentException("Invalid locale format: " + str); 324 } 325 final int len = str.length(); 326 if (len < 2) { 327 throw new IllegalArgumentException("Invalid locale format: " + str); 328 } 329 final char ch0 = str.charAt(0); 330 if (ch0 == UNDERSCORE || ch0 == DASH) { 331 if (len < 3) { 332 throw new IllegalArgumentException("Invalid locale format: " + str); 333 } 334 final char ch1 = str.charAt(1); 335 final char ch2 = str.charAt(2); 336 if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) { 337 throw new IllegalArgumentException("Invalid locale format: " + str); 338 } 339 if (len == 3) { 340 return new Locale(StringUtils.EMPTY, str.substring(1, 3)); 341 } 342 if (len < 5) { 343 throw new IllegalArgumentException("Invalid locale format: " + str); 344 } 345 if (str.charAt(3) != ch0) { 346 throw new IllegalArgumentException("Invalid locale format: " + str); 347 } 348 return new Locale(StringUtils.EMPTY, str.substring(1, 3), str.substring(4)); 349 } 350 351 return parseLocale(str); 352 } 353 354 /** 355 * {@link LocaleUtils} instances should NOT be constructed in standard programming. 356 * Instead, the class should be used as {@code LocaleUtils.toLocale("en_GB");}. 357 * 358 * <p>This constructor is public to permit tools that require a JavaBean instance 359 * to operate.</p> 360 */ 361 public LocaleUtils() { 362 } 363 364}