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 }