001    /*
002     * $Id: JDBCResources.java 354539 2005-12-06 20:40:16Z niallp $
003     * $Revision: 354539 $
004     * $Date: 2005-12-06 20:40:16 +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.io.FileNotFoundException;
027    import java.io.IOException;
028    import java.io.InputStream;
029    import java.net.URL;
030    import java.sql.Connection;
031    import java.sql.DriverManager;
032    import java.sql.PreparedStatement;
033    import java.sql.ResultSet;
034    import java.sql.SQLException;
035    import java.util.Locale;
036    import java.util.Map;
037    import java.util.Properties;
038    
039    import org.apache.commons.logging.Log;
040    import org.apache.commons.logging.LogFactory;
041    
042    /**
043     * <p>Concrete implementation of 
044     * {@link org.apache.commons.resources.Resources} that retrieves message
045     * key value pairs using JDBC. For this specific implementation, database
046     * connection properties and <code>Locale</code> for which the document's
047     * messages apply are specified via property files. Resources are looked up in
048     * a hierarchy of database values in a manner identical to that 
049     * performed by <code>java.util.ResourceBundle.getBundle().</code>.
050     * 
051     * </p>
052     *
053     * <p>The base URL passed to our constructor must contain the base name
054     * of a properties file that holds the JDBC datasource configuration.</p>
055     * 
056     * <p>
057     * The expected format of the required properties file might look like this:<br>
058     * <code><pre>
059     * jdbc.connect.driver               = org.gjt.mm.mysql.Driver
060     * jdbc.connect.url                  = jdbc:mysql://localhost/resources
061     * jdbc.connect.login                = resourcesTest
062     * jdbc.connect.password             = resourcesTest
063     * 
064     * jdbc.sql.db                       = resources
065     * jdbc.sql.table                    = resources
066     * jdbc.sql.locale.column            = locale
067     * jdbc.sql.key.column               = msgKey
068     * jdbc.sql.val.column               = val
069     * 
070     * org.apache.commons.resource.CACHE = true
071     * 
072     * </pre></code>
073     * 
074     * </p>
075     *
076     * @author James Mitchell
077     * @version $Revision: 354539 $ $Date: 2005-12-06 20:40:16 +0000 (Tue, 06 Dec 2005) $
078     */
079    public class JDBCResources extends CollectionResourcesBase {
080    
081        /**
082         * <p>The <code>Log</code> instance for this class.</p>
083         */
084        private transient Log log = LogFactory.getLog(JDBCResources.class);
085    
086        // ----------------------------------------------------------- Constructors
087    
088        /**
089         * <p>Create a new 
090         * {@link org.apache.commons.resources.Resources} instance with the specified
091         * logical name and base resource URL.</p>
092         *
093         * @param name Logical name of the new instance
094         * @param base Base URL of the JDBC configuration properties.
095         */
096        public JDBCResources(String name, String base) {
097            super(name, base);
098        }
099    
100    
101        // ------------------------------------------------------ Protected Methods
102    
103    
104        /**
105         * <p>Return a <code>Map</code> containing the name-value mappings for
106         * the specified base URL and requested <code>Locale</code>, if there
107         * are any.  If there are no defined mappings for the specified
108         * <code>Locale</code>, return an empty <code>Map</code> instead.</p>
109         *
110         * <p>Concrete subclasses must override this method to perform the
111         * appropriate lookup.  A typical implementation will construct an
112         * absolute URL based on the specified base URL and <code>Locale</code>,
113         * retrieve the specified resource file (if any), and parse it into
114         * a <code>Map</code> structure.</p>
115         *
116         * <p>Caching of previously retrieved <code>Map</code>s (if any) should
117         * be performed by callers of this method.  Therefore, this method should
118         * always attempt to retrieve the specified resource and load it
119         * appropriately.</p>
120         *
121         * @param baseUrl Base URL of the resource files for this 
122         * {@link org.apache.commons.resources.Resources} instance
123         * @param locale <code>Locale</code> for which name-value mappings
124         *  are requested
125         * @return A name-value Map for the specified URL and locale.
126         */
127        protected Map getLocaleMap(String baseUrl, Locale locale) {
128    
129            if (getLog().isDebugEnabled()) {
130                getLog().debug("Loading database configuration'" + locale + "' resources from base '" +
131                        baseUrl + "'");
132            }
133    
134            Properties props = new Properties();
135            //getLocaleSuffix(locale) + 
136            String name = baseUrl + ".properties";
137            InputStream stream = null;
138    
139            try {
140    
141                // Open an input stream to the URL for this locale (if any)
142                if (getLog().isTraceEnabled()) {
143                    getLog().trace("Absolute URL is '" + name + "'");
144                }
145                URL url = new URL(name);
146                stream = url.openStream();
147    
148                // Parse the input stream and populate the name-value mappings map
149                if (getLog().isTraceEnabled()) {
150                    getLog().trace("Parsing input resource");
151                }
152                props.load(stream);
153    
154            } catch (FileNotFoundException e) {
155    
156                // Log and swallow this exception
157                if (getLog().isDebugEnabled()) {
158                    getLog().debug("No resources for locale '" + locale +
159                              "' from base '" + baseUrl + "'");
160                }
161                props.clear();
162    
163            } catch (IOException e) {
164    
165                getLog().warn("IOException loading locale '" + locale +
166                         "' from base '" + baseUrl + "'", e);
167                props.clear();
168    
169            } finally {
170    
171                // Close the input stream that was opened earlier
172                if (stream != null) {
173                    try {
174                        stream.close();
175                    } catch (IOException e) {
176                        getLog().error("Error closing stream.", e);
177                    }
178                    stream = null;
179                }
180    
181            }
182    
183            // Return the populated (or empty) properties
184            Properties properties = new Properties();
185            try {
186                properties = loadData(locale, props);
187    
188            } catch (InstantiationException e) {
189                getLog().warn("InstantiationException: locale= '" + locale +
190                         "' base= '" + baseUrl + "'", e);
191            } catch (IllegalAccessException e) {
192                getLog().warn("IllegalAccessException: locale= '" + locale +
193                        "' base= '" + baseUrl + "'", e);
194            } catch (ClassNotFoundException e) {
195                getLog().warn("Specified Driver not found, make sure it is on " +
196                        "the classpath: locale= '" + locale +
197                        "' base= '" + baseUrl + "'", e);
198            } catch (SQLException e) {
199                getLog().warn("SQLException: locale= '" + locale +
200                        "' base= '" + baseUrl + "'", e);
201            }
202            return properties;
203    
204            
205    
206        }
207    
208        /**
209         * @param locale <code>Locale</code> for which name-value mappings
210         *  are requested
211         * @param connectionProps The connection properties used to instantiate
212         * a JDBC connection.
213         */
214        private Properties loadData(Locale locale, Properties connectionProps) 
215            throws InstantiationException, IllegalAccessException, 
216            ClassNotFoundException, SQLException {
217            
218            String driver = connectionProps.getProperty("jdbc.connect.driver");
219            String url    = connectionProps.getProperty("jdbc.connect.url");
220            String user = connectionProps.getProperty("jdbc.connect.login");
221            String pass = connectionProps.getProperty("jdbc.connect.password");
222            
223            String table = connectionProps.getProperty("jdbc.sql.table");
224            String localeColumn = connectionProps.getProperty("jdbc.sql.locale.column");
225            String keyColumn    = connectionProps.getProperty("jdbc.sql.key.column");
226            String valColumn    = connectionProps.getProperty("jdbc.sql.val.column");
227            Properties pairs = new Properties();
228            
229            Connection con = null;
230            PreparedStatement stmt = null;
231            ResultSet rs = null;
232            try {
233                Class.forName(driver).newInstance();
234                con = DriverManager.getConnection(url, user, pass);
235    
236                String query = "SELECT " + keyColumn + ", " + valColumn + 
237                        " FROM " + table + " where " + localeColumn + "= '" + locale + "'";
238                stmt = con.prepareStatement(query);
239                rs = stmt.executeQuery();
240    
241                while (rs.next()) {
242                    pairs.put(rs.getString(keyColumn), rs.getString(valColumn));
243                }
244            } finally {
245                try {
246                    if (rs != null) {
247                        rs.close();
248                    }
249                } catch(Exception e) {
250                    getLog().warn("Error closing ResultSet: " + e);
251                }
252                try {
253                    if (stmt != null) {
254                        stmt.close();
255                    }
256                } catch(Exception e) {
257                    getLog().warn("Error closing Statement: " + e);
258                }
259                try {
260                    if (con != null) {
261                        con.close();
262                    }
263                } catch(Exception e) {
264                    getLog().warn("Error closing Connection: " + e);
265                }
266            }
267    
268            return pairs;
269        }
270    
271        /**
272         * Accessor method for Log instance.
273         *
274         * The Log instance variable is transient and
275         * accessing it through this method ensures it
276         * is re-initialized when this instance is
277         * de-serialized.
278         *
279         * @return The Log instance.
280         */
281        private Log getLog() {
282            if (log == null) {
283                log =  LogFactory.getLog(JDBCResources.class);
284            }
285            return log;
286        }
287    }