1 /* 2 * $Id: CollectionResourcesBase.java 354330 2005-12-06 06:05:19Z niallp $ 3 * $Revision: 354330 $ 4 * $Date: 2005-12-06 06:05:19 +0000 (Tue, 06 Dec 2005) $ 5 * 6 * ==================================================================== 7 * 8 * Copyright 2003-2005 The Apache Software Foundation 9 * 10 * Licensed under the Apache License, Version 2.0 (the "License"); 11 * you may not use this file except in compliance with the License. 12 * You may obtain a copy of the License at 13 * 14 * http://www.apache.org/licenses/LICENSE-2.0 15 * 16 * Unless required by applicable law or agreed to in writing, software 17 * distributed under the License is distributed on an "AS IS" BASIS, 18 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 * See the License for the specific language governing permissions and 20 * limitations under the License. 21 * 22 */ 23 24 package org.apache.commons.resources.impl; 25 26 import java.util.ArrayList; 27 import java.util.HashMap; 28 import java.util.HashSet; 29 import java.util.Iterator; 30 import java.util.List; 31 import java.util.Locale; 32 import java.util.Map; 33 import java.util.Set; 34 35 import org.apache.commons.logging.Log; 36 import org.apache.commons.logging.LogFactory; 37 import org.apache.commons.resources.ResourcesException; 38 import org.apache.commons.resources.ResourcesKeyException; 39 40 /** 41 * <p>Abstract base classes for 42 * {@link org.apache.commons.resources.Resources} implementations that 43 * store their name-value mappings for each supported <code>Locale</code> 44 * in a URL-accessible resource file with a common base URL. Subclasses 45 * need only override <code>loadLocale()</code> to manage the details of 46 * loading the name-value mappings for a particular Locale.</p> 47 */ 48 public abstract class CollectionResourcesBase extends ResourcesBase { 49 50 /** 51 * <p>The logging instance for this class.</p> 52 */ 53 private transient Log log = 54 LogFactory.getLog(CollectionResourcesBase.class); 55 56 /** 57 * <p>Create a new {@link org.apache.commons.resources.Resources} instance with the specified 58 * logical name and base URL.</p> 59 * 60 * @param name Logical name of the new instance 61 * @param base Base URL of the resource files that contain the per-Locale 62 * name-value mappings for this {@link org.apache.commons.resources.Resources} instance 63 */ 64 public CollectionResourcesBase(String name, String base) { 65 super(name); 66 this.base = base; 67 } 68 69 70 // ----------------------------------------------------- Instance Variables 71 72 73 /** 74 * <p>The base URL for the per-Locale resources files containing the 75 * name-value mappings for this {@link org.apache.commons.resources.Resources} instance.</p> 76 */ 77 private String base = null; 78 79 80 /** 81 * <p>The default <code>Locale</code> to use when no <code>Locale</code> 82 * is specified by the caller.</p> 83 */ 84 private Locale defaultLocale = Locale.getDefault(); 85 86 87 /** 88 * <p>The previously calculated <code>Locale</code> lists returned 89 * by <code>getLocaleList()</code>, keyed by <code>Locale</code>.</p> 90 */ 91 private Map lists = new HashMap(); 92 93 94 /** 95 * <p>The previously calculated name-value mappings <code>Map</code>s 96 * returned by <code>getLocaleMap()</code>, keyed by <code>Locale</code>. 97 * </p> 98 */ 99 private Map maps = new HashMap(); 100 101 102 // ------------------------------------------------------------- Properties 103 104 105 /** 106 * Set the default locale. 107 * @param defaultLocale The default Locale. 108 */ 109 public void setDefaultLocale(Locale defaultLocale) { 110 this.defaultLocale = defaultLocale; 111 } 112 113 /** 114 * Return the default locale. 115 * @return The default Locale. 116 */ 117 public Locale getDefaultLocale() { 118 return defaultLocale; 119 } 120 121 /** 122 * @see org.apache.commons.resources.impl.ResourcesBase#getKeys() 123 */ 124 public Iterator getKeys() { 125 126 synchronized (maps) { 127 128 Set results = new HashSet(); 129 Iterator locales = maps.keySet().iterator(); 130 while (locales.hasNext()) { 131 Locale locale = (Locale) locales.next(); 132 Map map = (Map) maps.get(locale); 133 results.addAll(map.keySet()); 134 } 135 return (results.iterator()); 136 137 } 138 139 140 141 142 } 143 144 145 // ---------------------------------------------- Content Retrieval Methods 146 147 148 /** 149 * <p>Return the content for the specified <code>key</code> as an 150 * Object, localized based on the specified <code>locale</code>. 151 * </p> 152 * 153 * @param key Identifier for the requested content 154 * @param locale Locale with which to localize retrieval, 155 * or <code>null</code> for the default Locale 156 * @return The content for the specified key. 157 * 158 * @exception ResourcesException if an error occurs retrieving or 159 * returning the requested content 160 * @exception ResourcesKeyException if the no value for the specified 161 * key was found, and <code>isReturnNull()</code> returns 162 * <code>false</code> 163 */ 164 public Object getObject(String key, Locale locale) { 165 166 if (getLog().isTraceEnabled()) { 167 getLog().trace("Retrieving message for key '" + key + "' and locale '" 168 + locale + "'"); 169 } 170 171 if (locale == null) { 172 locale = defaultLocale; 173 } 174 175 // Prepare local variables we will need 176 List list = getLocaleList(locale); 177 int n = list.size(); 178 179 // Search through the Locale hierarchy for this resource key 180 for (int i = 0; i < n; i++) { 181 Map map = getLocaleMap((Locale) list.get(i)); 182 if (map.containsKey(key)) { 183 Object object = map.get(key); 184 if (getLog().isTraceEnabled()) { 185 getLog().trace("Retrieved object for key '" + key + 186 "' and locale '" + locale + 187 "' is '" + object + "'"); 188 } 189 return object; 190 } 191 } 192 193 if (getLog().isTraceEnabled()) { 194 getLog().trace("No message found for key '" + key + 195 "' and locale '" + locale + "'"); 196 } 197 198 // No value for this key was located in the entire hierarchy 199 if (isReturnNull()) { 200 return (null); 201 } else { 202 throw new ResourcesKeyException(key); 203 } 204 205 } 206 207 208 // ------------------------------------------------------ Lifecycle Methods 209 210 211 /** 212 * <p>This method must be called when the manager of this resource 213 * decides that it's no longer needed. After this method is called, 214 * no further calls to any of the <code>getXxx()</code> methods are 215 * allowed.</p> 216 * 217 * @exception ResourcesException if an error occurs during finalization 218 */ 219 public void destroy() { 220 221 synchronized (lists) { 222 lists.clear(); 223 } 224 synchronized (maps) { 225 maps.clear(); 226 } 227 228 } 229 230 231 // ------------------------------------------------------ Protected Methods 232 233 234 /** 235 * <p>Return a <code>List</code> of Locales that should be searched 236 * when locating resources for the specified Locale. The returned 237 * list will start with the specified Locale itself, followed by Locales 238 * that do not specify variant, country, or language modifiers. For 239 * example, if you pass in a Locale for the <code>en_US_POSIX</code> 240 * combination, the returned list will have Locale instances for 241 * the following country/language/variant combinations:</p> 242 * <ul> 243 * <li><code>en_US_POSIX</code></li> 244 * <li><code>en_US</code></li> 245 * <li><code>en</code></li> 246 * <li>(zero-length country, language, and variant)</li> 247 * </ul> 248 * 249 * <p>The search order calculated by this method makes it easy for 250 * {@link org.apache.commons.resources.Resources} implementations to implement hierarchical search 251 * strategies similar to that employed by the standard Java class 252 * <code>java.util.ResourceBundle</code>.</p> 253 * 254 * @param locale Locale on which to base the list calculation 255 * @return A List of locales. 256 */ 257 protected List getLocaleList(Locale locale) { 258 259 synchronized (lists) { 260 261 // Optimized lookup of any previously cached Map for this Locale 262 List list = (List) lists.get(locale); 263 if (list != null) { 264 return (list); 265 } 266 267 // Calculate, cache, and return the list for this Locale 268 list = new ArrayList(); 269 String language = locale.getLanguage(); 270 int languageLength = language.length(); 271 String country = locale.getCountry(); 272 int countryLength = country.length(); 273 String variant = locale.getVariant(); 274 int variantLength = variant.length(); 275 276 list.add(locale); 277 if (variantLength > 0) { 278 list.add(new Locale(language, country, "")); 279 } 280 if ((countryLength > 0) && (languageLength > 0)) { 281 list.add(new Locale(language, "", "")); 282 } 283 if ((languageLength > 0) || (countryLength > 0)) { 284 list.add(new Locale("", "", "")); 285 } 286 lists.put(locale, list); 287 return (list); 288 289 } 290 291 } 292 293 294 /** 295 * <p>Return the <code>Map</code> to be used to resolve name-value 296 * mappings for the specified <code>Locale</code>. Caching is utilized 297 * to ensure that a call to <code>getLocaleMap(base,locale)</code> 298 * occurs only once per <code>Locale</code>.</p> 299 * 300 * @param locale Locale for which to return a name-value mappings Map 301 * @return A name-value Map for the specified locale. 302 */ 303 protected Map getLocaleMap(Locale locale) { 304 305 synchronized (maps) { 306 307 // Optimized lookup of any previously cached Map for this Locale 308 Map map = (Map) maps.get(locale); 309 if (map != null) { 310 return (map); 311 } 312 313 // Calculate, cache, and return the map for this Locale 314 map = getLocaleMap(base, locale); 315 maps.put(locale, map); 316 return (map); 317 318 } 319 320 } 321 322 323 /** 324 * <p>Return a <code>Map</code> containing the name-value mappings for 325 * the specified base URL and requested <code>Locale</code>, if there 326 * are any. If there are no defined mappings for the specified 327 * <code>Locale</code>, return an empty <code>Map</code> instead.</p> 328 * 329 * <p>Concrete subclasses must override this method to perform the 330 * appropriate lookup. A typical implementation will construct an 331 * absolute URL based on the specified base URL and <code>Locale</code>, 332 * retrieve the specified resource file (if any), and parse it into 333 * a <code>Map</code> structure.</p> 334 * 335 * <p>Caching of previously retrieved <code>Map</code>s (if any) should 336 * be performed by callers of this method. Therefore, this method should 337 * always attempt to retrieve the specified resource and load it 338 * appropriately.</p> 339 * 340 * @param baseUrl Base URL of the resource files for this 341 * {@link org.apache.commons.resources.Resources} instance 342 * @param locale <code>Locale</code> for which name-value mappings 343 * are requested 344 * @return A name-value Map for the specified URL and locale. 345 */ 346 protected abstract Map getLocaleMap(String baseUrl, Locale locale); 347 348 349 /** 350 * <p>Return the <code>Locale</code>-specific suffix for the specified 351 * <code>Locale</code>. If the specified <code>Locale</code> has 352 * zero-length language and country components, the returned suffix 353 * will also have zero length. Otherwise, it will contain an 354 * underscore character followed by the non-zero-length language, 355 * country, and variant properties (separated by underscore characters). 356 * 357 * @param locale <code>Locale</code> for which a suffix string 358 * is requested 359 * @return The locale specific suffix. 360 */ 361 protected String getLocaleSuffix(Locale locale) { 362 363 if (locale == null) { 364 locale = defaultLocale; 365 } 366 String language = locale.getLanguage(); 367 if (language == null) { 368 language = ""; 369 } 370 String country = locale.getCountry(); 371 if (country == null) { 372 country = ""; 373 } 374 if ((language.length() < 1) && (country.length() < 1)) { 375 return (""); 376 } 377 StringBuffer sb = new StringBuffer(); 378 if (language.length() > 0) { 379 sb.append('_'); 380 sb.append(language.toLowerCase()); 381 } 382 if (country.length() > 0) { 383 sb.append('_'); 384 sb.append(country.toUpperCase()); 385 } 386 String variant = locale.getVariant(); 387 if ((variant != null) && (variant.length() > 0)) { 388 sb.append('_'); 389 sb.append(variant); 390 } 391 return (sb.toString()); 392 393 } 394 395 /** 396 * Accessor method for Log instance. 397 * 398 * The Log instance variable is transient and 399 * accessing it through this method ensures it 400 * is re-initialized when this instance is 401 * de-serialized. 402 * 403 * @return The Log instance. 404 */ 405 private Log getLog() { 406 if (log == null) { 407 log = LogFactory.getLog(CollectionResourcesBase.class); 408 } 409 return log; 410 } 411 412 }