001    /*
002     * $Id: XMLResources.java 348371 2005-11-23 04:56:51Z niallp $
003     * $Revision: 348371 $
004     * $Date: 2005-11-23 04:56:51 +0000 (Wed, 23 Nov 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.io.FileNotFoundException;
027    import java.io.IOException;
028    import java.io.InputStream;
029    import java.net.URL;
030    import java.util.HashMap;
031    import java.util.Locale;
032    import java.util.Map;
033    
034    import org.apache.commons.digester.Digester;
035    import org.apache.commons.logging.Log;
036    import org.apache.commons.logging.LogFactory;
037    import org.xml.sax.SAXException;
038    
039    /**
040     * <p>Concrete implementation of 
041     * {@link org.apache.commons.resources.Resources} that wraps a family
042     * (one per <code>Locale</code> of XML documents that share a base URL
043     * and have name suffixes reflecting the <code>Locale</code> for which
044     * the document's messages apply.  Resources are looked up in a hierarchy
045     * XML documents in a manner identical to that performed by
046     * <code>java.util.ResourceBundle.getBundle().</code>.</p>
047     *
048     * <p>The base URL passed to our constructor must contain the base name
049     * of the XML document family.
050     * For example, if the configuration URL is passed as
051     * <code>http://localhost/foo/Bar</code>, the resources for the
052     * <code>en_US</code> Locale would be stored under URL
053     * <code>http://localhost/foo/Bar_en_US.xml</code>, and the default
054     * resources would be stored in
055     * <code>http://localhost/foo/Bar.xml</code>.</p>
056     *
057     * <p>The required structure of the XML documents is very simple:</p>
058     * <ul>
059     * <li>The top level element must be <code>&lt;resources&gt;</code>.</li>
060     * <li>Each name-value pair is represented by a nested
061     *     <code>&lt;resource&gt;</code> element.</li>
062     * <li>For each <code>&lt;resource&gt;</code> element, the <code>id</code>
063     *     attribute contains the resource key, and the body contains a
064     *     string representation of the value.</li>
065     * </ul>
066     */
067    public class XMLResources extends CollectionResourcesBase {
068    
069        /**
070         * <p>The <code>Log</code> instance for this class.</p>
071         */
072        private transient Log log = LogFactory.getLog(XMLResources.class);
073    
074        // ----------------------------------------------------------- Constructors
075    
076    
077        /**
078         * <p>Create a new {@link org.apache.commons.resources.Resources} instance with the specified
079         * logical name and base resource URL.</p>
080         *
081         * @param name Logical name of the new instance
082         * @param base Base URL of the family of properties files that contain
083         *  the resource keys and values
084         */
085        public XMLResources(String name, String base) {
086            super(name, base);
087        }
088    
089    
090        // ------------------------------------------------------ Protected Methods
091    
092    
093        /**
094         * <p>Return a <code>Map</code> containing the name-value mappings for
095         * the specified base URL and requested <code>Locale</code>, if there
096         * are any.  If there are no defined mappings for the specified
097         * <code>Locale</code>, return an empty <code>Map</code> instead.</p>
098         *
099         * <p>Concrete subclasses must override this method to perform the
100         * appropriate lookup.  A typical implementation will construct an
101         * absolute URL based on the specified base URL and <code>Locale</code>,
102         * retrieve the specified resource file (if any), and parse it into
103         * a <code>Map</code> structure.</p>
104         *
105         * <p>Caching of previously retrieved <code>Map</code>s (if any) should
106         * be performed by callers of this method.  Therefore, this method should
107         * always attempt to retrieve the specified resource and load it
108         * appropriately.</p>
109         *
110         * @param baseUrl Base URL of the resource files for this {@link org.apache.commons.resources.Resources}
111         *  instance
112         * @param locale <code>Locale</code> for which name-value mappings
113         *  are requested
114         * @return A name-value Map for the specified URL and locale.
115         */
116        protected Map getLocaleMap(String baseUrl, Locale locale) {
117    
118            if (getLog().isDebugEnabled()) {
119                getLog().debug("Loading locale '" + locale + "' resources from base '" +
120                        baseUrl + "'");
121            }
122    
123            Map map = new HashMap();
124            String name = baseUrl + getLocaleSuffix(locale) + ".xml";
125            InputStream stream = null;
126    
127            try {
128    
129                // Open an input stream to the URL for this locale (if any)
130                if (getLog().isTraceEnabled()) {
131                    getLog().trace("Absolute URL is '" + name + "'");
132                }
133                URL url = new URL(name);
134                stream = url.openStream();
135    
136                // Create and configure a new Digester instance
137                if (getLog().isTraceEnabled()) {
138                    getLog().trace("Creating Digester instance");
139                }
140                Digester digester = new Digester();
141                digester.setNamespaceAware(false);
142                digester.setValidating(false);
143                digester.push(map);
144                digester.addCallMethod("resources/resource", "put", 2,
145                                       new String[] { "java.lang.Object",
146                                                      "java.lang.Object" });
147                digester.addCallParam("resources/resource", 0, "id");
148                digester.addCallParam("resources/resource", 1);
149    
150                // Parse the input stream and populate the name-value mappings map
151                if (getLog().isTraceEnabled()) {
152                    getLog().trace("Parsing input resource");
153                }
154                digester.parse(stream);
155    
156            } catch (FileNotFoundException e) {
157    
158                // Log and swallow this exception
159                if (getLog().isDebugEnabled()) {
160                    getLog().debug("No resources for locale '" + locale +
161                              "' from base '" + baseUrl + "'");
162                }
163                map.clear();
164    
165            } catch (IOException e) {
166    
167                // Log and swallow this exception
168                getLog().warn("IOException loading locale '" + locale +
169                         "' from base '" + baseUrl + "'", e);
170                map.clear();
171    
172            } catch (SAXException e) {
173    
174                // Log and swallow this exception
175                getLog().warn("SAXException loading locale '" + locale +
176                         "' from base '" + baseUrl + "'", e);
177                map.clear();
178    
179            } finally {
180    
181                // Close the input stream that was opened earlier
182                if (stream != null) {
183                    try {
184                        stream.close();
185                    } catch (IOException e) {
186                        getLog().error("Error closing stream.", e);
187                    }
188                    stream = null;
189                }
190    
191            }
192    
193            // Return the populated (or empty) map
194            return (map);
195    
196        }
197    
198        /**
199         * Accessor method for Log instance.
200         *
201         * The Log instance variable is transient and
202         * accessing it through this method ensures it
203         * is re-initialized when this instance is
204         * de-serialized.
205         *
206         * @return The Log instance.
207         */
208        private Log getLog() {
209            if (log == null) {
210                log =  LogFactory.getLog(XMLResources.class);
211            }
212            return log;
213        }
214    
215    }