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 * <resource-ref> 79 * <description>Velocity template DataSource</description> 80 * <res-ref-name>jdbc/Velocity</res-ref-name> 81 * <res-type>javax.sql.DataSource</res-type> 82 * <res-auth>Container</res-auth> 83 * </resource-ref> 84 * </code></pre> 85 * <br> 86 * and Tomcat 4 server.xml file: <br> 87 * <pre><code> 88 * [...] 89 * <Context path="/exampleVelocity" docBase="exampleVelocity" debug="0"> 90 * [...] 91 * <ResourceParams name="jdbc/Velocity"> 92 * <parameter> 93 * <name>driverClassName</name> 94 * <value>org.hsql.jdbcDriver</value> 95 * </parameter> 96 * <parameter> 97 * <name>driverName</name> 98 * <value>jdbc:HypersonicSQL:database</value> 99 * </parameter> 100 * <parameter> 101 * <name>user</name> 102 * <value>database_username</value> 103 * </parameter> 104 * <parameter> 105 * <name>password</name> 106 * <value>database_password</value> 107 * </parameter> 108 * </ResourceParams> 109 * [...] 110 * </Context> 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