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    
018    package org.apache.commons.resources.impl;
019    
020    import java.util.ArrayList;
021    import java.util.HashMap;
022    import java.util.HashSet;
023    import java.util.Iterator;
024    import java.util.List;
025    import java.util.Locale;
026    import java.util.Map;
027    import java.util.Set;
028    
029    import org.apache.commons.logging.Log;
030    import org.apache.commons.logging.LogFactory;
031    import org.apache.commons.resources.ResourcesException;
032    import org.apache.commons.resources.ResourcesKeyException;
033    
034    /**
035     * <p>Abstract base classes for
036     * {@link org.apache.commons.resources.Resources} implementations that
037     * store their name-value mappings for each supported <code>Locale</code>
038     * in a URL-accessible resource file with a common base URL.  Subclasses
039     * need only override <code>loadLocale()</code> to manage the details of
040     * loading the name-value mappings for a particular Locale.</p>
041     */
042    public abstract class CollectionResourcesBase extends ResourcesBase {
043    
044        /**
045         * <p>The logging instance for this class.</p>
046         */
047        private transient Log log =
048            LogFactory.getLog(CollectionResourcesBase.class);
049    
050        /**
051         * <p>Create a new {@link org.apache.commons.resources.Resources} instance with the specified
052         * logical name and base URL.</p>
053         *
054         * @param name Logical name of the new instance
055         * @param base Base URL of the resource files that contain the per-Locale
056         *  name-value mappings for this {@link org.apache.commons.resources.Resources} instance
057         */
058        public CollectionResourcesBase(String name, String base) {
059            super(name);
060            this.base = base;
061        }
062    
063    
064        // ----------------------------------------------------- Instance Variables
065    
066    
067        /**
068         * <p>The base URL for the per-Locale resources files containing the
069         * name-value mappings for this {@link org.apache.commons.resources.Resources} instance.</p>
070         */
071        private String base = null;
072    
073    
074        /**
075         * <p>The default <code>Locale</code> to use when no <code>Locale</code>
076         * is specified by the caller.</p>
077         */
078        private Locale defaultLocale = Locale.getDefault();
079    
080    
081        /**
082         * <p>The previously calculated <code>Locale</code> lists returned
083         * by <code>getLocaleList()</code>, keyed by <code>Locale</code>.</p>
084         */
085        private Map lists = new HashMap();
086    
087    
088        /**
089         * <p>The previously calculated name-value mappings <code>Map</code>s
090         * returned by <code>getLocaleMap()</code>, keyed by <code>Locale</code>.
091         * </p>
092         */
093        private Map maps = new HashMap();
094    
095    
096        // ------------------------------------------------------------- Properties
097    
098    
099        /**
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    }