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