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