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