This is an automated email from the ASF dual-hosted git repository.

cbrisson pushed a commit to branch VELOCITY-965
in repository https://gitbox.apache.org/repos/asf/velocity-engine.git


The following commit(s) were added to refs/heads/VELOCITY-965 by this push:
     new 7d98e5ef Complete refactoring of database objects handling in 
DataSourceResourceLoader
7d98e5ef is described below

commit 7d98e5ef890e1f758532ee25df9ef67499dfc37e
Author: Claude Brisson <[email protected]>
AuthorDate: Wed Aug 28 12:05:12 2024 +0200

    Complete refactoring of database objects handling in 
DataSourceResourceLoader
---
 .../loader/CachingDatabaseObjectsFactory.java      | 153 +++++++++++
 .../resource/loader/DataSourceResourceLoader.java  | 291 +++++++--------------
 .../resource/loader/DatabaseObjectsFactory.java    |  39 +++
 .../loader/DefaultDatabaseObjectsFactory.java      |  53 ++++
 .../test/sql/DataSourceResourceLoaderTestCase.java |  22 +-
 ...sourceResourceLoaderCachingFactoryTestCase.java |  19 ++
 6 files changed, 376 insertions(+), 201 deletions(-)

diff --git 
a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/CachingDatabaseObjectsFactory.java
 
b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/CachingDatabaseObjectsFactory.java
new file mode 100644
index 00000000..57999610
--- /dev/null
+++ 
b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/CachingDatabaseObjectsFactory.java
@@ -0,0 +1,153 @@
+package org.apache.velocity.runtime. resource.loader;
+
+import org.apache.commons.pool2.BasePooledObjectFactory;
+import org.apache.commons.pool2.PooledObject;
+import org.apache.commons.pool2.impl.DefaultPooledObject;
+import org.apache.commons.pool2.impl.GenericObjectPool;
+import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
+import org.apache.velocity.util.ExtProperties;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * <p>Database objects factory which will keep a single connection to be able 
to cache statements preparation, by means
+ * of appropriate pools.</p>
+ * <p>To use this class, you must add the following property to the example 
configuration described in
+ * @link{org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader}
+ * </p>
+ * <pre><code>
+ * resource.loader.ds.database_objects_factory.class = 
org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader<br>
+ * </code></pre>
+ * <p>The default size of each pool of prepared statements (there is one pool 
per statement) is 50. You can tune it
+ * with:</p>
+ * <pre><code>
+ * resource.loader.ds.database_objects_factory. = 
org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader<br>
+ * </code></pre>
+ * @see org.apache.velocity.runtime.resource.loader.DataSourceResourceLoader
+ */
+
+public class CachingDatabaseObjectsFactory implements DatabaseObjectsFactory {
+
+    private static final String STATEMENTS_POOL_MAX_SIZE = 
"statements_pool_max_size";
+    private static final int STATEMENTS_POOL_MAX_SIZE_DEFAULT = 50;
+
+    private DataSource dataSource;
+    private Connection connection;
+    private int poolsMaxSize;
+    private Map<String, GenericObjectPool<PreparedStatement>> statementsCache 
= new HashMap<>();
+
+    private class PreparedStatementFactory  extends 
BasePooledObjectFactory<PreparedStatement>
+    {
+        private final String sql;
+
+        PreparedStatementFactory(String sql)
+        {
+            this.sql = sql;
+        }
+
+        @Override
+        public PreparedStatement create() throws Exception {
+            checkConnection();
+            return connection.prepareStatement(sql);
+        }
+
+        @Override
+        public PooledObject<PreparedStatement> wrap(PreparedStatement obj) {
+            return new DefaultPooledObject<>(obj);
+        }
+
+        @Override
+        public void destroyObject(final PooledObject<PreparedStatement> p) 
throws Exception  {
+            p.getObject().close();
+        }
+    }
+
+    /**
+     * Initialize the factory with the DataSourceResourceLoader properties
+     * @param dataSource data source
+     */
+    @Override
+    public void init(DataSource dataSource, ExtProperties properties) throws 
SQLException
+    {
+        this.dataSource = dataSource;
+        this.connection = dataSource.getConnection();
+        this.poolsMaxSize = 
Optional.ofNullable(properties.getInt(STATEMENTS_POOL_MAX_SIZE)).orElse(STATEMENTS_POOL_MAX_SIZE_DEFAULT);
+    }
+
+    /**
+     * Prepare a statement
+     * @param sql Statement SQL
+     * @return prepared statement
+     */
+    @Override
+    public synchronized PreparedStatement prepareStatement(String sql) throws 
SQLException
+    {
+        GenericObjectPool<PreparedStatement> pool = 
statementsCache.computeIfAbsent(sql, (String key) -> {
+            GenericObjectPoolConfig<PreparedStatement> poolConfig = new 
GenericObjectPoolConfig<>();
+            poolConfig.setMaxTotal(poolsMaxSize);
+            return new GenericObjectPool<>(
+                    new PreparedStatementFactory(sql),
+                    poolConfig
+            );
+        });
+        try
+        {
+            return pool.borrowObject();
+        }
+        catch (SQLException sqle)
+        {
+            throw sqle;
+        }
+        catch (Exception e)
+        {
+            throw new SQLException("could not prepare statement", e);
+        }
+    }
+
+    private void checkConnection() throws SQLException
+    {
+        if (!connection.isValid(0))
+        {
+            // refresh connection
+            connection = dataSource.getConnection();
+            statementsCache = new HashMap<>();
+        }
+    }
+
+    /**
+     * Releases a prepared statement
+     * @param sql original sql query
+     * @param stmt statement
+     */
+    @Override
+    public void releaseStatement(String sql, PreparedStatement stmt) throws 
SQLException
+    {
+        GenericObjectPool<PreparedStatement> pool = statementsCache.get(sql);
+        if (pool == null)
+        {
+            throw new SQLException("statement is not pooled");
+        }
+        pool.returnObject(stmt);
+    }
+
+    @Override
+    public void destroy()
+    {
+        statementsCache.values().forEach(pool ->
+        {
+            pool.close();
+            pool.clear();
+        });
+        try
+        {
+            connection.close();
+        }
+        catch (SQLException sqle) {}
+    };
+}
diff --git 
a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java
 
b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java
index 9270023a..ecb827f6 100644
--- 
a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java
+++ 
b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DataSourceResourceLoader.java
@@ -19,15 +19,11 @@ package org.apache.velocity.runtime.resource.loader;
  * under the License.
  */
 
-import org.apache.commons.pool2.BasePooledObjectFactory;
 import org.apache.commons.pool2.ObjectPool;
-import org.apache.commons.pool2.PooledObject;
-import org.apache.commons.pool2.impl.DefaultPooledObject;
-import org.apache.commons.pool2.impl.GenericObjectPool;
-import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
 import org.apache.velocity.exception.ResourceNotFoundException;
 import org.apache.velocity.exception.VelocityException;
 import org.apache.velocity.runtime.resource.Resource;
+import org.apache.velocity.util.ClassUtils;
 import org.apache.velocity.util.ExtProperties;
 
 import org.apache.commons.lang3.StringUtils;
@@ -38,7 +34,6 @@ import javax.sql.DataSource;
 import java.io.FilterReader;
 import java.io.IOException;
 import java.io.Reader;
-import java.sql.Connection;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
@@ -126,6 +121,14 @@ import java.sql.Timestamp;
  *    template_timestamp datetime NOT NULL
  *  );
  * </code></pre>
+ * <p>Prior to Velocity 2.4, this class should not be considered 
thread-safe.</p>
+ * <p>Since Velocity 2.4, the handling of JDBC connections and prepared 
statements is delegated to the
+ * {@link org.apache.velocity.runtime.resource.loader.DatabaseObjectsFactory} 
instance. The default class for this
+ * database objets factory is {@link 
org.apache.velocity.runtime.resource.loader.DefaultDatabaseObjectsFactory},
+ * which obtain a new connection from the data source and prepare statements 
at each query. You can configure this
+ * resource loader to use the {@link 
org.apache.velocity.runtime.resource.loader.CachingDatabaseObjectsFactory} which
+ * will keep a single connection and tries to reuse prepared statements.
+ * statements</p>
  *
  * @author <a href="mailto:[email protected]";>Will Glass-Husain</a>
  * @author <a href="mailto:[email protected]";>Matt Raible</a>
@@ -138,61 +141,24 @@ import java.sql.Timestamp;
  */
 public class DataSourceResourceLoader extends ResourceLoader
 {
-    private static final int STATEMENTS_POOL_MAX_SIZE_DEFAULT = 50;
-    private String dataSourceName;
-    private String tableName;
-    private String keyColumn;
+    private static final String DATABASE_OBJECTS_FACTORY_DEFAULT_CLASS = 
"org.apache.velocity.runtime.resource.loader.DefaultDatabaseObjectsFactory";
+
+    private DataSource dataSource;
+    private DatabaseObjectsFactory factory;
     private String templateColumn;
     private String timestampColumn;
-    private InitialContext ctx;
-    private DataSource dataSource;
-
-    /*
-        Keep connection open.
-     */
-    private Connection connection = null;
-
-    /*
-        Keep two pools for prepared statements
-     */
-    private GenericObjectPool<PreparedStatement> templatePrepStatementsPool;
-    private GenericObjectPool<PreparedStatement> timestampPrepStatementsPool;
-
-    private class PreparedStatementFactory  extends 
BasePooledObjectFactory<PreparedStatement>
-    {
-        private final String selectColumn;
-
-        PreparedStatementFactory(String selectColumn)
-        {
-            this.selectColumn = selectColumn;
-        }
-
-        @Override
-        public PreparedStatement create() throws Exception {
-            return prepareStatement(connection, selectColumn, tableName, 
keyColumn);
-        }
+    private String templateSQL;
+    private String timestampSQL;
 
-        @Override
-        public PooledObject<PreparedStatement> wrap(PreparedStatement obj) {
-            return new DefaultPooledObject<>(obj);
-        }
-
-        @Override
-        public void destroyObject(final PooledObject<PreparedStatement> p) 
throws Exception  {
-            p.getObject().close();
-        }
-    }
-
-    private static class SelfCleaningReader extends FilterReader
+    private class SelfCleaningReader extends FilterReader
     {
         private ResultSet resultSet;
         private ObjectPool<PreparedStatement> statementPool;
 
-        public SelfCleaningReader(Reader reader, ResultSet resultSet, 
ObjectPool<PreparedStatement> statementPool)
+        public SelfCleaningReader(Reader reader, ResultSet resultSet)
         {
             super(reader);
             this.resultSet = resultSet;
-            this.statementPool = statementPool;
         }
 
         @Override
@@ -202,7 +168,7 @@ public class DataSourceResourceLoader extends ResourceLoader
             try
             {
                 resultSet.close();
-                
statementPool.returnObject((PreparedStatement)resultSet.getStatement());
+                factory.releaseStatement(templateSQL, 
(PreparedStatement)resultSet.getStatement());
             }
             catch (RuntimeException re)
             {
@@ -221,24 +187,36 @@ public class DataSourceResourceLoader extends 
ResourceLoader
     @Override
     public void init(ExtProperties configuration)
     {
-        dataSourceName  = 
StringUtils.trim(configuration.getString("resource.datasource_url"));
-        tableName       = 
StringUtils.trim(configuration.getString("resource.table"));
-        keyColumn       = 
StringUtils.trim(configuration.getString("resource.key_column"));
+        String tableName       = 
StringUtils.trim(configuration.getString("resource.table"));
+        String keyColumn       = 
StringUtils.trim(configuration.getString("resource.key_column"));
         templateColumn  = 
StringUtils.trim(configuration.getString("resource.template_column"));
         timestampColumn = 
StringUtils.trim(configuration.getString("resource.timestamp_column"));
 
+        templateSQL = "SELECT " + templateColumn + " FROM " + tableName + " 
WHERE " + keyColumn + " = ?";
+        timestampSQL = "SELECT " + timestampColumn + " FROM " + tableName + " 
WHERE " + keyColumn + " = ?";
+
+        String dataSourceName  = 
StringUtils.trim(configuration.getString("resource.datasource_url"));
+
         if (dataSource != null)
         {
             log.debug("DataSourceResourceLoader: using dataSource instance 
with table \"{}\"", tableName);
             log.debug("DataSourceResourceLoader: using columns \"{}\", \"{}\" 
and \"{}\"", keyColumn, templateColumn, timestampColumn);
 
-            log.trace("DataSourceResourceLoader initialized.");
         }
         else if (dataSourceName != null)
         {
             log.debug("DataSourceResourceLoader: using \"{}\" datasource with 
table \"{}\"", dataSourceName, tableName);
             log.debug("DataSourceResourceLoader: using columns \"{}\", \"{}\" 
and \"{}\"", keyColumn, templateColumn, timestampColumn);
 
+            try
+            {
+                dataSource = (DataSource) new 
InitialContext().lookup(dataSourceName);
+            }
+            catch (NamingException ne)
+            {
+                throw new Error("could not lookup datasource for name: " + 
dataSourceName, ne);
+            }
+
             log.trace("DataSourceResourceLoader initialized.");
         }
         else
@@ -248,20 +226,23 @@ public class DataSourceResourceLoader extends 
ResourceLoader
             throw new RuntimeException(msg);
         }
 
-        /* initialize statements pools */
-        int poolsMaxSize = configuration.getInt("statements_pool_max_size", 
STATEMENTS_POOL_MAX_SIZE_DEFAULT);
-        GenericObjectPoolConfig<PreparedStatement> poolConfig = new 
GenericObjectPoolConfig<>();
-        poolConfig.setMaxTotal(poolsMaxSize);
-
-        templatePrepStatementsPool = new GenericObjectPool<>(
-                new PreparedStatementFactory(templateColumn),
-                poolConfig
-        );
+        String factoryClassName = 
configuration.getString("database_objects_factory.class");
+        if (factoryClassName == null)
+        {
+            factoryClassName = DATABASE_OBJECTS_FACTORY_DEFAULT_CLASS;
+        }
+        try
+        {
+            Class<?> factoryClass = ClassUtils.getClass(factoryClassName);
+            factory = (DatabaseObjectsFactory) 
factoryClass.getDeclaredConstructor().newInstance();
+            factory.init(dataSource, 
configuration.subset("database_objects_factory"));
+        }
+        catch (Exception e)
+        {
+            throw new Error("could not find database objects factory class", 
e);
+        }
 
-        timestampPrepStatementsPool = new GenericObjectPool<>(
-                new PreparedStatementFactory(timestampColumn),
-                poolConfig
-        );
+        log.trace("DataSourceResourceLoader initialized.");
     }
 
     /**
@@ -271,6 +252,10 @@ public class DataSourceResourceLoader extends 
ResourceLoader
      */
     public void setDataSource(final DataSource dataSource)
     {
+        if (factory != null)
+        {
+            throw new Error("cannot change data source after initialization");
+        }
         this.dataSource = dataSource;
     }
 
@@ -315,8 +300,7 @@ public class DataSourceResourceLoader extends ResourceLoader
         ResultSet rs = null;
         try
         {
-            checkDBConnection();
-            PreparedStatement statement = 
templatePrepStatementsPool.borrowObject();
+            PreparedStatement statement = 
factory.prepareStatement(templateSQL);
             rs = fetchResult(statement, name);
 
             if (rs.next())
@@ -328,7 +312,7 @@ public class DataSourceResourceLoader extends ResourceLoader
                             + "template column for '"
                             + name + "' is null");
                 }
-                return new SelfCleaningReader(reader, rs, 
templatePrepStatementsPool);
+                return new SelfCleaningReader(reader, rs);
             }
             else
             {
@@ -374,8 +358,7 @@ public class DataSourceResourceLoader extends ResourceLoader
             ResultSet rs = null;
             try
             {
-                checkDBConnection();
-                statement = timestampPrepStatementsPool.borrowObject();
+                statement = factory.prepareStatement(timestampSQL);
                 rs = fetchResult(statement, name);
 
                 if (rs.next())
@@ -385,8 +368,7 @@ public class DataSourceResourceLoader extends ResourceLoader
                 }
                 else
                 {
-                    String msg = "DataSourceResourceLoader: could not find 
resource "
-                              + name + " while " + operation;
+                    String msg = "DataSourceResourceLoader: could not find 
resource " + name + " while " + operation;
                     log.error(msg);
                     throw new ResourceNotFoundException(msg);
                 }
@@ -402,99 +384,23 @@ public class DataSourceResourceLoader extends 
ResourceLoader
             finally
             {
                 closeResultSet(rs);
-                if (statement != null) {
-                    timestampPrepStatementsPool.returnObject(statement);
+                if (statement != null)
+                {
+                    try
+                    {
+                        factory.releaseStatement(timestampSQL, statement);
+                    }
+                    catch (SQLException sqle)
+                    {
+                        // just log, don't throw
+                        log.error("DataSourceResourceLoader: error releasing 
prepared statement", sqle);
+                    }
                 }
             }
         }
         return timeStamp;
     }
 
-    /**
-     * Gets connection to the datasource specified through the configuration
-     * parameters.
-     *
-     */
-    private void openDBConnection() throws NamingException, SQLException
-    {
-        if (dataSource == null)
-        {
-            if (ctx == null)
-            {
-                ctx = new InitialContext();
-            }
-
-            dataSource = (DataSource) ctx.lookup(dataSourceName);
-        }
-
-        if (connection != null)
-        {
-            closeDBConnection();
-        }
-
-        connection = dataSource.getConnection();
-    }
-
-    /**
-     * Checks the connection is valid
-     *
-     */
-    private void checkDBConnection() throws NamingException, SQLException
-    {
-        if (connection == null || !connection.isValid(0))
-        {
-            openDBConnection();
-        }
-    }
-
-    /**
-     * Close DB connection on finalization
-     *
-     * @throws Throwable
-     */
-    @Override
-    protected void finalize()
-        throws Throwable
-    {
-        closeDBConnection();
-    }
-
-    /**
-     * Closes the prepared statements and the connection to the datasource
-     */
-    private void closeDBConnection()
-    {
-        if (templatePrepStatementsPool != null)
-        {
-            templatePrepStatementsPool.close();
-            templatePrepStatementsPool.clear();
-        }
-        if (timestampPrepStatementsPool != null)
-        {
-            timestampPrepStatementsPool.close();
-            timestampPrepStatementsPool.clear();
-        }
-        if (connection != null)
-        {
-            try
-            {
-                connection.close();
-            }
-            catch (RuntimeException re)
-            {
-                throw re;
-            }
-            catch (SQLException e)
-            {
-                // ignore
-            }
-            finally
-            {
-                connection = null;
-            }
-        }
-    }
-
     /**
      * Closes the result set.
      */
@@ -517,32 +423,6 @@ public class DataSourceResourceLoader extends 
ResourceLoader
         }
     }
 
-    /**
-     * Creates the following PreparedStatement query :
-     * <br>
-     *  SELECT <i>columnNames</i> FROM <i>tableName</i> WHERE <i>keyColumn</i>
-     *     = '<i>templateName</i>'
-     * <br>
-     * where <i>keyColumn</i> is a class member set in init()
-     *
-     * @param conn connection to datasource
-     * @param columnNames columns to fetch from datasource
-     * @param tableName table to fetch from
-     * @param keyColumn column whose value should match templateName
-     * @return PreparedStatement
-     * @throws SQLException
-     */
-    protected PreparedStatement prepareStatement(
-        final Connection conn,
-        final String columnNames,
-        final String tableName,
-        final String keyColumn
-    ) throws SQLException
-    {
-        PreparedStatement ps = conn.prepareStatement("SELECT " + columnNames + 
" FROM "+ tableName + " WHERE " + keyColumn + " = ?");
-        return ps;
-    }
-
     /**
      * Fetches the result for a given template name.
      * Inherit this method if there is any calculation to perform on the 
template name.
@@ -562,17 +442,42 @@ public class DataSourceResourceLoader extends 
ResourceLoader
     }
 
     /**
-     * Gets a reader from a result set's column
-     * @param resultSet
-     * @param column
-     * @param encoding
+     * Gets a reader from a result set's column.
+     * @param resultSet result set
+     * @param column template column
+     * @param encoding template encoding (unused)
      * @return reader
      * @throws SQLException
+     * @deprecated the 'encoding' parameter is useless, it should have been 
set in the database. Use {@link #getReader(ResultSet, String)}
      */
     protected Reader getReader(ResultSet resultSet, String column, String 
encoding)
         throws SQLException
+    {
+        return getReader(resultSet, column);
+    }
+
+    /**
+     * Gets a reader from a result set's column.
+     * @param resultSet result set
+     * @param column template column
+     * @return reader
+     * @throws SQLException
+     */
+    protected Reader getReader(ResultSet resultSet, String column)
+            throws SQLException
     {
         return resultSet.getCharacterStream(column);
     }
 
+    /**
+     * Frees all resources.
+     */
+    public void destroy()
+    {
+        if (factory != null)
+        {
+            factory.destroy();
+        }
+    }
+
 }
diff --git 
a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DatabaseObjectsFactory.java
 
b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DatabaseObjectsFactory.java
new file mode 100644
index 00000000..34c9d8dc
--- /dev/null
+++ 
b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DatabaseObjectsFactory.java
@@ -0,0 +1,39 @@
+package org.apache.velocity.runtime.resource.loader;
+
+import org.apache.velocity.util.ExtProperties;
+
+import javax.sql.DataSource;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+/**
+ * Factory for creating connections and prepared statements
+ */
+
+public interface DatabaseObjectsFactory {
+
+    /**
+     * Initialize the factory with the DataSourceResourceLoader properties
+     * @param dataSource data source
+     */
+    void init(DataSource dataSource, ExtProperties properties) throws 
SQLException;
+
+    /**
+     * Prepare a statement
+     * @param sql Statement SQL
+     * @return prepared statement
+     */
+    PreparedStatement prepareStatement(String sql) throws SQLException;
+
+    /**
+     * Releases a prepared statement
+     * @param sql original sql query
+     * @param stmt statement
+     */
+    void releaseStatement(String sql, PreparedStatement stmt) throws 
SQLException;
+
+    /**
+     * Free resources
+     */
+    default void destroy() {};
+}
diff --git 
a/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DefaultDatabaseObjectsFactory.java
 
b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DefaultDatabaseObjectsFactory.java
new file mode 100644
index 00000000..ed52d5a2
--- /dev/null
+++ 
b/velocity-engine-core/src/main/java/org/apache/velocity/runtime/resource/loader/DefaultDatabaseObjectsFactory.java
@@ -0,0 +1,53 @@
+package org.apache.velocity.runtime.resource.loader;
+
+import org.apache.velocity.util.ExtProperties;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+
+/**
+ * Database objects factory which will obtain a new connection from the data 
source and prepare needed statements
+ * at each call
+ */
+
+public class DefaultDatabaseObjectsFactory implements DatabaseObjectsFactory {
+
+    private DataSource dataSource;
+
+    /**
+     * Initialize the factory with the DataSourceResourceLoader properties
+     * @param dataSource data source
+     */
+    @Override
+    public void init(DataSource dataSource, ExtProperties properties)
+    {
+        this.dataSource = dataSource;
+    }
+
+    /**
+     * Prepare a statement
+     * @param sql Statement SQL
+     * @return prepared statement
+     */
+    @Override
+    public PreparedStatement prepareStatement(String sql) throws SQLException
+    {
+        Connection connection = dataSource.getConnection();
+        return connection.prepareStatement(sql);
+    }
+
+    /**
+     * Releases a prepared statement
+     * @param sql original sql query
+     * @param stmt statement
+     */
+    @Override
+    public void releaseStatement(String sql, PreparedStatement stmt) throws 
SQLException
+    {
+        Connection connection = stmt.getConnection();
+        stmt.close();
+        connection.close();
+    }
+}
diff --git 
a/velocity-engine-core/src/test/java/org/apache/velocity/test/sql/DataSourceResourceLoaderTestCase.java
 
b/velocity-engine-core/src/test/java/org/apache/velocity/test/sql/DataSourceResourceLoaderTestCase.java
index 8e4caa4d..d4333b73 100644
--- 
a/velocity-engine-core/src/test/java/org/apache/velocity/test/sql/DataSourceResourceLoaderTestCase.java
+++ 
b/velocity-engine-core/src/test/java/org/apache/velocity/test/sql/DataSourceResourceLoaderTestCase.java
@@ -98,14 +98,9 @@ public class DataSourceResourceLoaderTestCase
         DataSourceResourceLoader rl2 = new DataSourceResourceLoader();
         rl2.setDataSource(ds2);
 
-        ExtProperties props = new ExtProperties();
-        props.addProperty( "resource.loader", "ds" );
+        ExtProperties props = getResourceLoaderProperties();
         props.setProperty( "ds.resource.loader.instance", rl1);
-        props.setProperty( "ds.resource.loader.resource.table",           
"velocity_template_varchar");
-        props.setProperty( "ds.resource.loader.resource.keycolumn",       
"vt_id");
-        props.setProperty( "ds.resource.loader.resource.templatecolumn",  
"vt_def");
-        props.setProperty( "ds.resource.loader.resource.timestampcolumn", 
"vt_timestamp");
-        props.setProperty(Velocity.RUNTIME_LOG_INSTANCE, new TestLogger(false, 
false));
+        props.setProperty( "ds.resource.loader.resource.table", 
"velocity_template_varchar");
 
         varcharTemplatesEngine = new RuntimeInstance();
         varcharTemplatesEngine.setConfiguration(props);
@@ -113,12 +108,23 @@ public class DataSourceResourceLoaderTestCase
 
         ExtProperties props2 = (ExtProperties)props.clone();
         props2.setProperty( "ds.resource.loader.instance", rl2);
-        props2.setProperty( "ds.resource.loader.resource.table",           
"velocity_template_clob");
+        props2.setProperty( "ds.resource.loader.resource.table",  
"velocity_template_clob");
         clobTemplatesEngine = new RuntimeInstance();
         clobTemplatesEngine.setConfiguration(props2);
         clobTemplatesEngine.init();
     }
 
+    protected ExtProperties getResourceLoaderProperties()
+    {
+        ExtProperties props = new ExtProperties();
+        props.addProperty( "resource.loader", "ds" );
+        props.setProperty( "ds.resource.loader.resource.keycolumn",       
"vt_id");
+        props.setProperty( "ds.resource.loader.resource.templatecolumn",  
"vt_def");
+        props.setProperty( "ds.resource.loader.resource.timestampcolumn", 
"vt_timestamp");
+        props.setProperty(Velocity.RUNTIME_LOG_INSTANCE, new TestLogger(false, 
false));
+        return props;
+    }
+
     /**
      * Tests loading and rendering of a simple template. If that works, we are 
able to get data
      * from the database.
diff --git 
a/velocity-engine-core/src/test/java/org/apache/velocity/test/sql/DatasourceResourceLoaderCachingFactoryTestCase.java
 
b/velocity-engine-core/src/test/java/org/apache/velocity/test/sql/DatasourceResourceLoaderCachingFactoryTestCase.java
new file mode 100644
index 00000000..7f5e487b
--- /dev/null
+++ 
b/velocity-engine-core/src/test/java/org/apache/velocity/test/sql/DatasourceResourceLoaderCachingFactoryTestCase.java
@@ -0,0 +1,19 @@
+package org.apache.velocity.test.sql;
+
+import org.apache.velocity.util.ExtProperties;
+
+public class DatasourceResourceLoaderCachingFactoryTestCase extends 
DataSourceResourceLoaderTestCase
+{
+    public DatasourceResourceLoaderCachingFactoryTestCase(String name) throws 
Exception {
+        super(name);
+    }
+
+    @Override
+    protected ExtProperties getResourceLoaderProperties()
+    {
+        ExtProperties props = super.getResourceLoaderProperties();
+        props.put("ds.database_objects_factory.class", 
"org.apache.velocity.resource.loader.CachingDatabaseObjectsFactory");
+        return props;
+    }
+
+}

Reply via email to