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