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