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 }