001    /*
002     * $Id: CollectionResourcesBase.java 354330 2005-12-06 06:05:19Z niallp $
003     * $Revision: 354330 $
004     * $Date: 2005-12-06 06:05:19 +0000 (Tue, 06 Dec 2005) $
005     *
006     * ====================================================================
007     *
008     *  Copyright 2003-2005 The Apache Software Foundation
009     *
010     *  Licensed under the Apache License, Version 2.0 (the "License");
011     *  you may not use this file except in compliance with the License.
012     *  You may obtain a copy of the License at
013     *
014     *      http://www.apache.org/licenses/LICENSE-2.0
015     *
016     *  Unless required by applicable law or agreed to in writing, software
017     *  distributed under the License is distributed on an "AS IS" BASIS,
018     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
019     *  See the License for the specific language governing permissions and
020     *  limitations under the License.
021     *
022     */
023    
024    package org.apache.commons.resources.impl;
025    
026    import java.util.ArrayList;
027    import java.util.HashMap;
028    import java.util.HashSet;
029    import java.util.Iterator;
030    import java.util.List;
031    import java.util.Locale;
032    import java.util.Map;
033    import java.util.Set;
034    
035    import org.apache.commons.logging.Log;
036    import org.apache.commons.logging.LogFactory;
037    import org.apache.commons.resources.ResourcesException;
038    import org.apache.commons.resources.ResourcesKeyException;
039    
040    /**
041     * <p>Abstract base classes for 
042     * {@link org.apache.commons.resources.Resources} implementations that
043     * store their name-value mappings for each supported <code>Locale</code>
044     * in a URL-accessible resource file with a common base URL.  Subclasses
045     * need only override <code>loadLocale()</code> to manage the details of
046     * loading the name-value mappings for a particular Locale.</p>
047     */
048    public abstract class CollectionResourcesBase extends ResourcesBase {
049    
050        /**
051         * <p>The logging instance for this class.</p>
052         */
053        private transient Log log =
054            LogFactory.getLog(CollectionResourcesBase.class);
055    
056        /**
057         * <p>Create a new {@link org.apache.commons.resources.Resources} instance with the specified
058         * logical name and base URL.</p>
059         *
060         * @param name Logical name of the new instance
061         * @param base Base URL of the resource files that contain the per-Locale
062         *  name-value mappings for this {@link org.apache.commons.resources.Resources} instance
063         */
064        public CollectionResourcesBase(String name, String base) {
065            super(name);
066            this.base = base;
067        }
068    
069    
070        // ----------------------------------------------------- Instance Variables
071    
072    
073        /**
074         * <p>The base URL for the per-Locale resources files containing the
075         * name-value mappings for this {@link org.apache.commons.resources.Resources} instance.</p>
076         */
077        private String base = null;
078    
079    
080        /**
081         * <p>The default <code>Locale</code> to use when no <code>Locale</code>
082         * is specified by the caller.</p>
083         */
084        private Locale defaultLocale = Locale.getDefault();
085    
086    
087        /**
088         * <p>The previously calculated <code>Locale</code> lists returned
089         * by <code>getLocaleList()</code>, keyed by <code>Locale</code>.</p>
090         */
091        private Map lists = new HashMap();
092    
093    
094        /**
095         * <p>The previously calculated name-value mappings <code>Map</code>s
096         * returned by <code>getLocaleMap()</code>, keyed by <code>Locale</code>.
097         * </p>
098         */
099        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    }