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 }