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