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 */ 017 package org.apache.commons.lang3; 018 019 import java.util.ArrayList; 020 import java.util.Arrays; 021 import java.util.Collections; 022 import java.util.HashMap; 023 import java.util.HashSet; 024 import java.util.List; 025 import java.util.Locale; 026 import java.util.Map; 027 import java.util.Set; 028 029 /** 030 * <p>Operations to assist when working with a {@link Locale}.</p> 031 * 032 * <p>This class tries to handle <code>null</code> input gracefully. 033 * An exception will not be thrown for a <code>null</code> input. 034 * Each method documents its behaviour in more detail.</p> 035 * 036 * @author Apache Software Foundation 037 * @since 2.2 038 * @version $Id: LocaleUtils.java 889215 2009-12-10 11:56:38Z bayard $ 039 */ 040 public class LocaleUtils { 041 042 /** Unmodifiable list of available locales. */ 043 //@GuardedBy("this") 044 private static List<Locale> cAvailableLocaleList; // lazily created by availableLocaleList() 045 046 /** Unmodifiable set of available locales. */ 047 //@GuardedBy("this") 048 private static Set<Locale> cAvailableLocaleSet; // lazily created by availableLocaleSet() 049 050 /** Unmodifiable map of language locales by country. */ 051 private static final Map<String, List<Locale>> cLanguagesByCountry = Collections.synchronizedMap(new HashMap<String, List<Locale>>()); 052 053 /** Unmodifiable map of country locales by language. */ 054 private static final Map<String, List<Locale>> cCountriesByLanguage = Collections.synchronizedMap(new HashMap<String, List<Locale>>()); 055 056 /** 057 * <p><code>LocaleUtils</code> instances should NOT be constructed in standard programming. 058 * Instead, the class should be used as <code>LocaleUtils.toLocale("en_GB");</code>.</p> 059 * 060 * <p>This constructor is public to permit tools that require a JavaBean instance 061 * to operate.</p> 062 */ 063 public LocaleUtils() { 064 super(); 065 } 066 067 //----------------------------------------------------------------------- 068 /** 069 * <p>Converts a String to a Locale.</p> 070 * 071 * <p>This method takes the string format of a locale and creates the 072 * locale object from it.</p> 073 * 074 * <pre> 075 * LocaleUtils.toLocale("en") = new Locale("en", "") 076 * LocaleUtils.toLocale("en_GB") = new Locale("en", "GB") 077 * LocaleUtils.toLocale("en_GB_xxx") = new Locale("en", "GB", "xxx") (#) 078 * </pre> 079 * 080 * <p>(#) The behaviour of the JDK variant constructor changed between JDK1.3 and JDK1.4. 081 * In JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't. 082 * Thus, the result from getVariant() may vary depending on your JDK.</p> 083 * 084 * <p>This method validates the input strictly. 085 * The language code must be lowercase. 086 * The country code must be uppercase. 087 * The separator must be an underscore. 088 * The length must be correct. 089 * </p> 090 * 091 * @param str the locale String to convert, null returns null 092 * @return a Locale, null if null input 093 * @throws IllegalArgumentException if the string is an invalid format 094 */ 095 public static Locale toLocale(String str) { 096 if (str == null) { 097 return null; 098 } 099 int len = str.length(); 100 if (len != 2 && len != 5 && len < 7) { 101 throw new IllegalArgumentException("Invalid locale format: " + str); 102 } 103 char ch0 = str.charAt(0); 104 char ch1 = str.charAt(1); 105 if (ch0 < 'a' || ch0 > 'z' || ch1 < 'a' || ch1 > 'z') { 106 throw new IllegalArgumentException("Invalid locale format: " + str); 107 } 108 if (len == 2) { 109 return new Locale(str, ""); 110 } else { 111 if (str.charAt(2) != '_') { 112 throw new IllegalArgumentException("Invalid locale format: " + str); 113 } 114 char ch3 = str.charAt(3); 115 if (ch3 == '_') { 116 return new Locale(str.substring(0, 2), "", str.substring(4)); 117 } 118 char ch4 = str.charAt(4); 119 if (ch3 < 'A' || ch3 > 'Z' || ch4 < 'A' || ch4 > 'Z') { 120 throw new IllegalArgumentException("Invalid locale format: " + str); 121 } 122 if (len == 5) { 123 return new Locale(str.substring(0, 2), str.substring(3, 5)); 124 } else { 125 if (str.charAt(5) != '_') { 126 throw new IllegalArgumentException("Invalid locale format: " + str); 127 } 128 return new Locale(str.substring(0, 2), str.substring(3, 5), str.substring(6)); 129 } 130 } 131 } 132 133 //----------------------------------------------------------------------- 134 /** 135 * <p>Obtains the list of locales to search through when performing 136 * a locale search.</p> 137 * 138 * <pre> 139 * localeLookupList(Locale("fr","CA","xxx")) 140 * = [Locale("fr","CA","xxx"), Locale("fr","CA"), Locale("fr")] 141 * </pre> 142 * 143 * @param locale the locale to start from 144 * @return the unmodifiable list of Locale objects, 0 being locale, never null 145 */ 146 public static List<Locale> localeLookupList(Locale locale) { 147 return localeLookupList(locale, locale); 148 } 149 150 //----------------------------------------------------------------------- 151 /** 152 * <p>Obtains the list of locales to search through when performing 153 * a locale search.</p> 154 * 155 * <pre> 156 * localeLookupList(Locale("fr", "CA", "xxx"), Locale("en")) 157 * = [Locale("fr","CA","xxx"), Locale("fr","CA"), Locale("fr"), Locale("en"] 158 * </pre> 159 * 160 * <p>The result list begins with the most specific locale, then the 161 * next more general and so on, finishing with the default locale. 162 * The list will never contain the same locale twice.</p> 163 * 164 * @param locale the locale to start from, null returns empty list 165 * @param defaultLocale the default locale to use if no other is found 166 * @return the unmodifiable list of Locale objects, 0 being locale, never null 167 */ 168 public static List<Locale> localeLookupList(Locale locale, Locale defaultLocale) { 169 List<Locale> list = new ArrayList<Locale>(4); 170 if (locale != null) { 171 list.add(locale); 172 if (locale.getVariant().length() > 0) { 173 list.add(new Locale(locale.getLanguage(), locale.getCountry())); 174 } 175 if (locale.getCountry().length() > 0) { 176 list.add(new Locale(locale.getLanguage(), "")); 177 } 178 if (list.contains(defaultLocale) == false) { 179 list.add(defaultLocale); 180 } 181 } 182 return Collections.unmodifiableList(list); 183 } 184 185 //----------------------------------------------------------------------- 186 /** 187 * <p>Obtains an unmodifiable list of installed locales.</p> 188 * 189 * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}. 190 * It is more efficient, as the JDK method must create a new array each 191 * time it is called.</p> 192 * 193 * @return the unmodifiable list of available locales 194 */ 195 public static List<Locale> availableLocaleList() { 196 if(cAvailableLocaleList == null) { 197 initAvailableLocaleList(); 198 } 199 return cAvailableLocaleList; 200 } 201 202 /** 203 * Initializes the availableLocaleList. It is separate from availableLocaleList() 204 * to avoid the synchronized block affecting normal use, yet synchronized and 205 * lazy loading to avoid a static block affecting other methods in this class. 206 */ 207 private static synchronized void initAvailableLocaleList() { 208 if(cAvailableLocaleList == null) { 209 List<Locale> list = Arrays.asList(Locale.getAvailableLocales()); 210 cAvailableLocaleList = Collections.unmodifiableList(list); 211 } 212 } 213 214 //----------------------------------------------------------------------- 215 /** 216 * <p>Obtains an unmodifiable set of installed locales.</p> 217 * 218 * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}. 219 * It is more efficient, as the JDK method must create a new array each 220 * time it is called.</p> 221 * 222 * @return the unmodifiable set of available locales 223 */ 224 public static Set<Locale> availableLocaleSet() { 225 if(cAvailableLocaleSet == null) { 226 initAvailableLocaleSet(); 227 } 228 return cAvailableLocaleSet; 229 } 230 231 /** 232 * Initializes the availableLocaleSet. It is separate from availableLocaleSet() 233 * to avoid the synchronized block affecting normal use, yet synchronized and 234 * lazy loading to avoid a static block affecting other methods in this class. 235 */ 236 private static synchronized void initAvailableLocaleSet() { 237 if(cAvailableLocaleSet == null) { 238 cAvailableLocaleSet = Collections.unmodifiableSet( new HashSet<Locale>(availableLocaleList()) ); 239 } 240 } 241 242 //----------------------------------------------------------------------- 243 /** 244 * <p>Checks if the locale specified is in the list of available locales.</p> 245 * 246 * @param locale the Locale object to check if it is available 247 * @return true if the locale is a known locale 248 */ 249 public static boolean isAvailableLocale(Locale locale) { 250 return availableLocaleList().contains(locale); 251 } 252 253 //----------------------------------------------------------------------- 254 /** 255 * <p>Obtains the list of languages supported for a given country.</p> 256 * 257 * <p>This method takes a country code and searches to find the 258 * languages available for that country. Variant locales are removed.</p> 259 * 260 * @param countryCode the 2 letter country code, null returns empty 261 * @return an unmodifiable List of Locale objects, never null 262 */ 263 public static List<Locale> languagesByCountry(String countryCode) { 264 List<Locale> langs = cLanguagesByCountry.get(countryCode); //syncd 265 if (langs == null) { 266 if (countryCode != null) { 267 langs = new ArrayList<Locale>(); 268 List<Locale> locales = availableLocaleList(); 269 for (int i = 0; i < locales.size(); i++) { 270 Locale locale = locales.get(i); 271 if (countryCode.equals(locale.getCountry()) && 272 locale.getVariant().length() == 0) { 273 langs.add(locale); 274 } 275 } 276 langs = Collections.unmodifiableList(langs); 277 } else { 278 langs = Collections.emptyList(); 279 } 280 cLanguagesByCountry.put(countryCode, langs); //syncd 281 } 282 return langs; 283 } 284 285 //----------------------------------------------------------------------- 286 /** 287 * <p>Obtains the list of countries supported for a given language.</p> 288 * 289 * <p>This method takes a language code and searches to find the 290 * countries available for that language. Variant locales are removed.</p> 291 * 292 * @param languageCode the 2 letter language code, null returns empty 293 * @return an unmodifiable List of Locale objects, never null 294 */ 295 public static List<Locale> countriesByLanguage(String languageCode) { 296 List<Locale> countries = cCountriesByLanguage.get(languageCode); //syncd 297 if (countries == null) { 298 if (languageCode != null) { 299 countries = new ArrayList<Locale>(); 300 List<Locale> locales = availableLocaleList(); 301 for (int i = 0; i < locales.size(); i++) { 302 Locale locale = locales.get(i); 303 if (languageCode.equals(locale.getLanguage()) && 304 locale.getCountry().length() != 0 && 305 locale.getVariant().length() == 0) { 306 countries.add(locale); 307 } 308 } 309 countries = Collections.unmodifiableList(countries); 310 } else { 311 countries = Collections.emptyList(); 312 } 313 cCountriesByLanguage.put(languageCode, countries); //syncd 314 } 315 return countries; 316 } 317 318 }