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