View Javadoc

1   package org.apache.velocity.runtime.resource.loader;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.    
20   */
21  
22  import java.io.BufferedInputStream;
23  import java.io.InputStream;
24  import java.sql.Connection;
25  import java.sql.PreparedStatement;
26  import java.sql.ResultSet;
27  import java.sql.SQLException;
28  import java.sql.Timestamp;
29  import javax.naming.InitialContext;
30  import javax.naming.NamingException;
31  import javax.sql.DataSource;
32  import org.apache.commons.collections.ExtendedProperties;
33  import org.apache.velocity.exception.ResourceNotFoundException;
34  import org.apache.velocity.exception.VelocityException;
35  import org.apache.velocity.runtime.resource.Resource;
36  import org.apache.velocity.util.ExceptionUtils;
37  import org.apache.velocity.util.StringUtils;
38  
39  /**
40   * <P>This is a simple template file loader that loads templates
41   * from a DataSource instead of plain files.
42   *
43   * <P>It can be configured with a datasource name, a table name,
44   * id column (name), content column (the template body) and a
45   * datetime column (for last modification info).
46   * <br>
47   * <br>
48   * Example configuration snippet for velocity.properties:
49   * <br>
50   * <br>
51   * resource.loader = file, ds <br>
52   * <br>
53   * ds.resource.loader.public.name = DataSource <br>
54   * ds.resource.loader.description = Velocity DataSource Resource Loader <br>
55   * ds.resource.loader.class = org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader <br>
56   * ds.resource.loader.resource.datasource = java:comp/env/jdbc/Velocity <br>
57   * ds.resource.loader.resource.table = tb_velocity_template <br>
58   * ds.resource.loader.resource.keycolumn = id_template <br>
59   * ds.resource.loader.resource.templatecolumn = template_definition <br>
60   * ds.resource.loader.resource.timestampcolumn = template_timestamp <br>
61   * ds.resource.loader.cache = false <br>
62   * ds.resource.loader.modificationCheckInterval = 60 <br>
63   * <br>
64   * <P>Optionally, the developer can instantiate the DataSourceResourceLoader and set the DataSource via code in
65   * a manner similar to the following:
66   * <BR>
67   * <BR>
68   * DataSourceResourceLoader ds = new DataSourceResourceLoader();<BR>
69   * ds.setDataSource(DATASOURCE);<BR>
70   * Velocity.setProperty("ds.resource.loader.instance",ds);<BR>
71   * <P> The property <code>ds.resource.loader.class</code> should be left out, otherwise all the other
72   * properties in velocity.properties would remain the same.
73   * <BR>
74   * <BR>
75   *
76   * Example WEB-INF/web.xml: <br>
77   * <br>
78   *  <resource-ref> <br>
79   *   <description>Velocity template DataSource</description> <br>
80   *   <res-ref-name>jdbc/Velocity</res-ref-name> <br>
81   *   <res-type>javax.sql.DataSource</res-type> <br>
82   *   <res-auth>Container</res-auth> <br>
83   *  </resource-ref> <br>
84   * <br>
85   *  <br>
86   * and Tomcat 4 server.xml file: <br>
87   *  [...] <br>
88   *  <Context path="/exampleVelocity" docBase="exampleVelocity" debug="0"> <br>
89   *  [...] <br>
90   *   <ResourceParams name="jdbc/Velocity"> <br>
91   *    <parameter> <br>
92   *      <name>driverClassName</name> <br>
93   *      <value>org.hsql.jdbcDriver</value> <br>
94   *    </parameter> <br>
95   *    <parameter> <br>
96   *     <name>driverName</name> <br>
97   *     <value>jdbc:HypersonicSQL:database</value> <br>
98   *    </parameter> <br>
99   *    <parameter> <br>
100  *     <name>user</name> <br>
101  *     <value>database_username</value> <br>
102  *    </parameter> <br>
103  *    <parameter> <br>
104  *     <name>password</name> <br>
105  *     <value>database_password</value> <br>
106  *    </parameter> <br>
107  *   </ResourceParams> <br>
108  *  [...] <br>
109  *  </Context> <br>
110  *  [...] <br>
111  * <br>
112  *  Example sql script:<br>
113  *  CREATE TABLE tb_velocity_template ( <br>
114  *  id_template varchar (40) NOT NULL , <br>
115  *  template_definition text (16) NOT NULL , <br>
116  *  template_timestamp datetime NOT NULL  <br>
117  *  ) <br>
118  *
119  * @author <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
120  * @author <a href="mailto:matt@raibledesigns.com">Matt Raible</a>
121  * @author <a href="mailto:david.kinnvall@alertir.com">David Kinnvall</a>
122  * @author <a href="mailto:paulo.gaspar@krankikom.de">Paulo Gaspar</a>
123  * @author <a href="mailto:lachiewicz@plusnet.pl">Sylwester Lachiewicz</a>
124  * @author <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
125  * @version $Id: DataSourceResourceLoader.java 991660 2010-09-01 19:13:46Z nbubna $
126  * @since 1.5
127  */
128 public class DataSourceResourceLoader extends ResourceLoader
129 {
130     private String dataSourceName;
131     private String tableName;
132     private String keyColumn;
133     private String templateColumn;
134     private String timestampColumn;
135     private InitialContext ctx;
136     private DataSource dataSource;
137 
138     /**
139      * @see org.apache.velocity.runtime.resource.loader.ResourceLoader#init(org.apache.commons.collections.ExtendedProperties)
140      */
141     public void init(ExtendedProperties configuration)
142     {
143         dataSourceName  = StringUtils.nullTrim(configuration.getString("resource.datasource"));
144         tableName       = StringUtils.nullTrim(configuration.getString("resource.table"));
145         keyColumn       = StringUtils.nullTrim(configuration.getString("resource.keycolumn"));
146         templateColumn  = StringUtils.nullTrim(configuration.getString("resource.templatecolumn"));
147         timestampColumn = StringUtils.nullTrim(configuration.getString("resource.timestampcolumn"));
148 
149         if (dataSource != null)
150         {
151             if (log.isDebugEnabled())
152             {
153                 log.debug("DataSourceResourceLoader: using dataSource instance with table \""
154                           + tableName + "\"");
155                 log.debug("DataSourceResourceLoader: using columns \""
156                           + keyColumn + "\", \"" + templateColumn + "\" and \""
157                           + timestampColumn + "\"");
158             }
159 
160             log.trace("DataSourceResourceLoader initialized.");
161         }
162         else if (dataSourceName != null)
163         {
164             if (log.isDebugEnabled())
165             {
166                 log.debug("DataSourceResourceLoader: using \"" + dataSourceName
167                           + "\" datasource with table \"" + tableName + "\"");
168                 log.debug("DataSourceResourceLoader: using columns \""
169                           + keyColumn + "\", \"" + templateColumn + "\" and \""
170                           + timestampColumn + "\"");
171             }
172 
173             log.trace("DataSourceResourceLoader initialized.");
174         }
175         else
176         {
177             String msg = "DataSourceResourceLoader not properly initialized. No DataSource was identified.";
178             log.error(msg);
179             throw new RuntimeException(msg);
180         }
181     }
182 
183     /**
184      * Set the DataSource used by this resource loader.  Call this as an alternative to
185      * specifying the data source name via properties.
186      * @param dataSource The data source for this ResourceLoader.
187      */
188     public void setDataSource(final DataSource dataSource)
189     {
190         this.dataSource = dataSource;
191     }
192 
193     /**
194      * @see org.apache.velocity.runtime.resource.loader.ResourceLoader#isSourceModified(org.apache.velocity.runtime.resource.Resource)
195      */
196     public boolean isSourceModified(final Resource resource)
197     {
198         return (resource.getLastModified() !=
199                 readLastModified(resource, "checking timestamp"));
200     }
201 
202     /**
203      * @see org.apache.velocity.runtime.resource.loader.ResourceLoader#getLastModified(org.apache.velocity.runtime.resource.Resource)
204      */
205     public long getLastModified(final Resource resource)
206     {
207         return readLastModified(resource, "getting timestamp");
208     }
209 
210     /**
211      * Get an InputStream so that the Runtime can build a
212      * template with it.
213      *
214      *  @param name name of template
215      *  @return InputStream containing template
216      * @throws ResourceNotFoundException
217      */
218     public synchronized InputStream getResourceStream(final String name)
219         throws ResourceNotFoundException
220     {
221         if (org.apache.commons.lang.StringUtils.isEmpty(name))
222         {
223             throw new ResourceNotFoundException("DataSourceResourceLoader: Template name was empty or null");
224         }
225 
226         Connection conn = null;
227         ResultSet rs = null;
228         PreparedStatement ps = null;
229         try
230         {
231             conn = openDbConnection();
232             ps = getStatement(conn, templateColumn, name);
233             rs = ps.executeQuery();
234 
235             if (rs.next())
236             {
237                 InputStream stream = rs.getBinaryStream(templateColumn);
238                 if (stream == null)
239                 {
240                     throw new ResourceNotFoundException("DataSourceResourceLoader: "
241                                                         + "template column for '"
242                                                         + name + "' is null");
243                 }
244 
245                 return new BufferedInputStream(stream);
246             }
247             else
248             {
249                 throw new ResourceNotFoundException("DataSourceResourceLoader: "
250                                                     + "could not find resource '"
251                                                     + name + "'");
252 
253             }
254         }
255         catch (SQLException sqle)
256         {
257             String msg = "DataSourceResourceLoader: database problem while getting resource '"
258                          + name + "': ";
259 
260             log.error(msg, sqle);
261             throw new ResourceNotFoundException(msg);
262         }
263         catch (NamingException ne)
264         {
265             String msg = "DataSourceResourceLoader: database problem while getting resource '"
266                          + name + "': ";
267 
268             log.error(msg, ne);
269             throw new ResourceNotFoundException(msg);
270         }
271         finally
272         {
273             closeResultSet(rs);
274             closeStatement(ps);
275             closeDbConnection(conn);
276         }
277     }
278 
279     /**
280      * Fetches the last modification time of the resource
281      *
282      * @param resource Resource object we are finding timestamp of
283      * @param operation string for logging, indicating caller's intention
284      *
285      * @return timestamp as long
286      */
287     private long readLastModified(final Resource resource, final String operation)
288     {
289         long timeStamp = 0;
290 
291         /* get the template name from the resource */
292         String name = resource.getName();
293         if (name == null || name.length() == 0)
294         {
295             String msg = "DataSourceResourceLoader: Template name was empty or null";
296             log.error(msg);
297             throw new NullPointerException(msg);
298         }
299         else
300         {
301             Connection conn = null;
302             ResultSet rs = null;
303             PreparedStatement ps = null;
304 
305             try
306             {
307                 conn = openDbConnection();
308                 ps = getStatement(conn, timestampColumn, name);
309                 rs = ps.executeQuery();
310 
311                 if (rs.next())
312                 {
313                     Timestamp ts = rs.getTimestamp(timestampColumn);
314                     timeStamp = ts != null ? ts.getTime() : 0;
315                 }
316                 else
317                 {
318                     String msg = "DataSourceResourceLoader: could not find resource "
319                               + name + " while " + operation;
320                     log.error(msg);
321                     throw new ResourceNotFoundException(msg);
322                 }
323             }
324             catch (SQLException sqle)
325             {
326                 String msg = "DataSourceResourceLoader: database problem while "
327                             + operation + " of '" + name + "': ";
328 
329                 log.error(msg, sqle);
330                 throw ExceptionUtils.createRuntimeException(msg, sqle);
331             }
332             catch (NamingException ne)
333             {
334                 String msg = "DataSourceResourceLoader: database problem while "
335                              + operation + " of '" + name + "': ";
336 
337                 log.error(msg, ne);
338                 throw ExceptionUtils.createRuntimeException(msg, ne);
339             }
340             finally
341             {
342                 closeResultSet(rs);
343                 closeStatement(ps);
344                 closeDbConnection(conn);
345             }
346         }
347         return timeStamp;
348     }
349 
350     /**
351      * Gets connection to the datasource specified through the configuration
352      * parameters.
353      *
354      * @return connection
355      */
356     private Connection openDbConnection() throws NamingException, SQLException
357     {
358          if (dataSource != null)
359          {
360             return dataSource.getConnection();
361          }
362 
363          if (ctx == null)
364          {
365             ctx = new InitialContext();
366          }
367 
368          dataSource = (DataSource) ctx.lookup(dataSourceName);
369 
370          return dataSource.getConnection();
371      }
372 
373     /**
374      * Closes connection to the datasource
375      */
376     private void closeDbConnection(final Connection conn)
377     {
378         if (conn != null)
379         {
380             try
381             {
382                 conn.close();
383             }
384             catch (RuntimeException re)
385             {
386                 throw re;
387             }
388             catch (Exception e)
389             {
390                 String msg = "DataSourceResourceLoader: problem when closing connection";
391                 log.error(msg, e);
392                 throw new VelocityException(msg, e);
393             }
394         }
395     }
396 
397     /**
398      * Closes the result set.
399      */
400     private void closeResultSet(final ResultSet rs)
401     {
402         if (rs != null)
403         {
404             try
405             {
406                 rs.close();
407             }
408             catch (RuntimeException re)
409             {
410                 throw re;
411             }
412             catch (Exception e)
413             {
414                 String msg = "DataSourceResourceLoader: problem when closing result set";
415                 log.error(msg, e);
416                 throw new VelocityException(msg, e);
417             }
418         }
419     }
420     
421     /**
422      * Closes the PreparedStatement.
423      */
424     private void closeStatement(PreparedStatement ps)
425     {
426         if (ps != null)
427         {
428             try
429             {
430                 ps.close();
431             }
432             catch (RuntimeException re)
433             {
434                 throw re;
435             }
436             catch (Exception e)
437             {
438                 String msg = "DataSourceResourceLoader: problem when closing PreparedStatement ";
439                 log.error(msg, e);
440                 throw new VelocityException(msg, e);
441             }
442         }
443     }
444         
445 
446     /**
447      * Creates the following PreparedStatement query :
448      * <br>
449      *  SELECT <i>columnNames</i> FROM <i>tableName</i> WHERE <i>keyColumn</i>
450      *     = '<i>templateName</i>'
451      * <br>
452      * where <i>keyColumn</i> is a class member set in init()
453      *
454      * @param conn connection to datasource
455      * @param columnNames columns to fetch from datasource
456      * @param templateName name of template to fetch
457      * @return PreparedStatement
458      */
459     private PreparedStatement getStatement(final Connection conn,
460                                final String columnNames,
461                                final String templateName) throws SQLException
462     {
463         PreparedStatement ps = conn.prepareStatement("SELECT " + columnNames + " FROM "+ tableName + " WHERE " + keyColumn + " = ?");
464         ps.setString(1, templateName);
465         return ps;
466     }
467 
468 }