DBCP-566: clearStatementPoolOnReturn (#42)

* DBCP-566: clearStatementPoolOnReturn

* Removed redundant assert

inner2 is the same as inner1

* Javadoc updates

* New method isClearStatementPoolOnReturn needs to have a default implementation to retain binary compatibility

* Getter for statement pool

* Fixed spelling errors and missing Javadoc

Co-authored-by: Robert Paschek <robert.paschek@comm-unity.at>
diff --git a/src/main/java/org/apache/commons/dbcp2/BasicDataSource.java b/src/main/java/org/apache/commons/dbcp2/BasicDataSource.java
index 337c40d..cb96d46 100644
--- a/src/main/java/org/apache/commons/dbcp2/BasicDataSource.java
+++ b/src/main/java/org/apache/commons/dbcp2/BasicDataSource.java
@@ -221,6 +221,8 @@
      */
     private boolean poolPreparedStatements = false;
 
+    private boolean clearStatementPoolOnReturn = false;
+    
     /**
      * <p>
      * The maximum number of open statements that can be allocated from the statement pool at the same time, or negative
@@ -655,6 +657,7 @@
             connectionFactory.setDefaultSchema(defaultSchema);
             connectionFactory.setCacheState(cacheState);
             connectionFactory.setPoolStatements(poolPreparedStatements);
+            connectionFactory.setClearStatementPoolOnReturn(clearStatementPoolOnReturn);
             connectionFactory.setMaxOpenPreparedStatements(maxOpenPreparedStatements);
             connectionFactory.setMaxConnLifetimeMillis(maxConnLifetimeMillis);
             connectionFactory.setRollbackOnReturn(getRollbackOnReturn());
@@ -1509,6 +1512,17 @@
         return this.poolPreparedStatements;
     }
 
+    /**
+     * Returns true if the statement pool is cleared when the connection is returned to its pool.
+     * 
+     * @return true if the statement pool is cleared at connection return 
+     * @since 2.8.0
+     */
+    @Override
+    public boolean isClearStatementPoolOnReturn() {
+        return clearStatementPoolOnReturn;
+    }
+
     @Override
     public boolean isWrapperFor(final Class<?> iface) throws SQLException {
         return false;
@@ -2219,6 +2233,17 @@
     }
 
     /**
+     * Sets whether the pool of statements (which was enabled with {@link #setPoolPreparedStatements(boolean)}) should 
+     * be cleared when the connection is returned to its pool. Default is false.
+     * 
+     * @param clearStatementPoolOnReturn clear or not
+     * @since 2.8.0
+     */
+    public void setClearStatementPoolOnReturn(final boolean clearStatementPoolOnReturn) {
+        this.clearStatementPoolOnReturn = clearStatementPoolOnReturn;
+    }
+
+    /**
      * @param removeAbandonedOnBorrow true means abandoned connections may be removed when connections are borrowed from
      *                                the pool.
      * @see #getRemoveAbandonedOnBorrow()
diff --git a/src/main/java/org/apache/commons/dbcp2/BasicDataSourceFactory.java b/src/main/java/org/apache/commons/dbcp2/BasicDataSourceFactory.java
index 37a2ffe..d7bb1bd 100644
--- a/src/main/java/org/apache/commons/dbcp2/BasicDataSourceFactory.java
+++ b/src/main/java/org/apache/commons/dbcp2/BasicDataSourceFactory.java
@@ -100,6 +100,7 @@
     private static final String PROP_LOG_ABANDONED = "logAbandoned";
     private static final String PROP_ABANDONED_USAGE_TRACKING = "abandonedUsageTracking";
     private static final String PROP_POOL_PREPARED_STATEMENTS = "poolPreparedStatements";
+    private static final String PROP_CLEAR_STATEMENT_POOL_ON_RETURN = "clearStatementPoolOnReturn";
     private static final String PROP_MAX_OPEN_PREPARED_STATEMENTS = "maxOpenPreparedStatements";
     private static final String PROP_CONNECTION_PROPERTIES = "connectionProperties";
     private static final String PROP_MAX_CONN_LIFETIME_MILLIS = "maxConnLifetimeMillis";
@@ -140,6 +141,7 @@
             PROP_URL, PROP_USER_NAME, PROP_VALIDATION_QUERY, PROP_VALIDATION_QUERY_TIMEOUT, PROP_CONNECTION_INIT_SQLS,
             PROP_ACCESS_TO_UNDERLYING_CONNECTION_ALLOWED, PROP_REMOVE_ABANDONED_ON_BORROW, PROP_REMOVE_ABANDONED_ON_MAINTENANCE,
             PROP_REMOVE_ABANDONED_TIMEOUT, PROP_LOG_ABANDONED, PROP_ABANDONED_USAGE_TRACKING, PROP_POOL_PREPARED_STATEMENTS,
+            PROP_CLEAR_STATEMENT_POOL_ON_RETURN,
             PROP_MAX_OPEN_PREPARED_STATEMENTS, PROP_CONNECTION_PROPERTIES, PROP_MAX_CONN_LIFETIME_MILLIS,
             PROP_LOG_EXPIRED_CONNECTIONS, PROP_ROLLBACK_ON_RETURN, PROP_ENABLE_AUTO_COMMIT_ON_RETURN,
             PROP_DEFAULT_QUERY_TIMEOUT, PROP_FAST_FAIL_VALIDATION, PROP_DISCONNECTION_SQL_CODES, PROP_JMX_NAME,
@@ -490,6 +492,11 @@
             dataSource.setPoolPreparedStatements(Boolean.valueOf(value).booleanValue());
         }
 
+        value = properties.getProperty(PROP_CLEAR_STATEMENT_POOL_ON_RETURN);
+        if (value != null) {
+            dataSource.setClearStatementPoolOnReturn(Boolean.valueOf(value).booleanValue());
+        }
+
         value = properties.getProperty(PROP_MAX_OPEN_PREPARED_STATEMENTS);
         if (value != null) {
             dataSource.setMaxOpenPreparedStatements(Integer.parseInt(value));
diff --git a/src/main/java/org/apache/commons/dbcp2/BasicDataSourceMXBean.java b/src/main/java/org/apache/commons/dbcp2/BasicDataSourceMXBean.java
index 4754764..85f8a58 100644
--- a/src/main/java/org/apache/commons/dbcp2/BasicDataSourceMXBean.java
+++ b/src/main/java/org/apache/commons/dbcp2/BasicDataSourceMXBean.java
@@ -132,6 +132,16 @@
     boolean isPoolPreparedStatements();
 
     /**
+     * See {@link BasicDataSource#isClearStatementPoolOnReturn()}
+     * 
+     * @return {@link BasicDataSource#isClearStatementPoolOnReturn()}
+     * @since 2.8.0
+     */
+    default boolean isClearStatementPoolOnReturn() {
+        return false;
+    }
+    
+    /**
      * See {@link BasicDataSource#getMaxOpenPreparedStatements()}
      *
      * @return {@link BasicDataSource#getMaxOpenPreparedStatements()}
diff --git a/src/main/java/org/apache/commons/dbcp2/PoolableConnection.java b/src/main/java/org/apache/commons/dbcp2/PoolableConnection.java
index 6a6ccf4..4365a21 100644
--- a/src/main/java/org/apache/commons/dbcp2/PoolableConnection.java
+++ b/src/main/java/org/apache/commons/dbcp2/PoolableConnection.java
@@ -124,6 +124,9 @@
     protected void passivate() throws SQLException {
         super.passivate();
         setClosedInternal(true);
+        if (getDelegateInternal() instanceof PoolingConnection) {
+            ((PoolingConnection) getDelegateInternal()).connectionReturnedToPool();
+        }
     }
 
     /**
diff --git a/src/main/java/org/apache/commons/dbcp2/PoolableConnectionFactory.java b/src/main/java/org/apache/commons/dbcp2/PoolableConnectionFactory.java
index 50f4ab5..c744f49 100644
--- a/src/main/java/org/apache/commons/dbcp2/PoolableConnectionFactory.java
+++ b/src/main/java/org/apache/commons/dbcp2/PoolableConnectionFactory.java
@@ -84,6 +84,8 @@
 
     private boolean poolStatements;
 
+    private boolean clearStatementPoolOnReturn;
+
     private int maxOpenPreparedStatements = GenericKeyedObjectPoolConfig.DEFAULT_MAX_TOTAL_PER_KEY;
 
     private long maxConnLifetimeMillis = -1;
@@ -392,6 +394,7 @@
             final KeyedObjectPool<PStmtKey, DelegatingPreparedStatement> stmtPool = new GenericKeyedObjectPool<>(
                     poolingConn, config);
             poolingConn.setStatementPool(stmtPool);
+            poolingConn.setClearStatementPoolOnReturn(clearStatementPoolOnReturn);
             poolingConn.setCacheState(cacheState);
         }
 
@@ -597,6 +600,17 @@
         this.poolStatements = poolStatements;
     }
 
+    /**
+     * Sets whether the pool of statements (which was enabled with {@link #setPoolStatements(boolean)}) should 
+     * be cleared when the connection is returned to its pool. Default is false.
+     * 
+     * @param clearStatementPoolOnReturn clear or not
+     * @since 2.8.0
+     */
+    public void setClearStatementPoolOnReturn(final boolean clearStatementPoolOnReturn) {
+        this.clearStatementPoolOnReturn = clearStatementPoolOnReturn;
+    }
+    
     public void setRollbackOnReturn(final boolean rollbackOnReturn) {
         this.rollbackOnReturn = rollbackOnReturn;
     }
diff --git a/src/main/java/org/apache/commons/dbcp2/PoolingConnection.java b/src/main/java/org/apache/commons/dbcp2/PoolingConnection.java
index d99ed22..8237dc7 100644
--- a/src/main/java/org/apache/commons/dbcp2/PoolingConnection.java
+++ b/src/main/java/org/apache/commons/dbcp2/PoolingConnection.java
@@ -65,6 +65,8 @@
     /** Pool of {@link PreparedStatement}s. and {@link CallableStatement}s */
     private KeyedObjectPool<PStmtKey, DelegatingPreparedStatement> pstmtPool;
 
+    private boolean clearStatementPoolOnReturn = false;
+    
     /**
      * Constructor.
      *
@@ -578,6 +580,27 @@
         pstmtPool = pool;
     }
 
+    /**
+     * Returns the prepared statement pool we're using.
+     * 
+     * @return statement pool
+     * @since 2.8.0
+     */
+    public KeyedObjectPool<PStmtKey, DelegatingPreparedStatement> getStatementPool() {
+        return pstmtPool;
+    }
+    
+    /**
+     * Sets whether the pool of statements should be cleared when the connection is returned to its pool. 
+     * Default is false.
+     * 
+     * @param clearStatementPoolOnReturn clear or not
+     * @since 2.8.0
+     */
+    public void setClearStatementPoolOnReturn(final boolean clearStatementPoolOnReturn) {
+        this.clearStatementPoolOnReturn = clearStatementPoolOnReturn;
+    }
+    
     @Override
     public synchronized String toString() {
         if (pstmtPool != null) {
@@ -599,4 +622,21 @@
     public boolean validateObject(final PStmtKey key, final PooledObject<DelegatingPreparedStatement> pooledObject) {
         return true;
     }
+
+    /**
+     * Notification from {@link PoolableConnection} that we returned to the pool.
+     * 
+     * @throws SQLException when <code>clearStatementPoolOnReturn</code> is true and the statement pool could not be
+     *                      cleared
+     * @since 2.8.0
+     */
+    public void connectionReturnedToPool() throws SQLException {
+        if (pstmtPool != null && clearStatementPoolOnReturn) {
+            try {
+                pstmtPool.clear();
+            } catch (Exception e) {
+                throw new SQLException("Error clearing statement pool", e);
+            }
+        }
+    }
 }
diff --git a/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java b/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java
index 11dcbcd..0a3dae6 100644
--- a/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java
+++ b/src/main/java/org/apache/commons/dbcp2/managed/BasicManagedDataSource.java
@@ -237,6 +237,7 @@
             connectionFactory.setDefaultSchema(getDefaultSchema());
             connectionFactory.setCacheState(getCacheState());
             connectionFactory.setPoolStatements(isPoolPreparedStatements());
+            connectionFactory.setClearStatementPoolOnReturn(isClearStatementPoolOnReturn());
             connectionFactory.setMaxOpenPreparedStatements(getMaxOpenPreparedStatements());
             connectionFactory.setMaxConnLifetimeMillis(getMaxConnLifetimeMillis());
             connectionFactory.setRollbackOnReturn(getRollbackOnReturn());
diff --git a/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceFactory.java b/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceFactory.java
index 5ba12fd..83dcbea 100644
--- a/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceFactory.java
+++ b/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceFactory.java
@@ -137,6 +137,7 @@
         properties.setProperty("logAbandoned", "true");
         properties.setProperty("abandonedUsageTracking", "true");
         properties.setProperty("poolPreparedStatements", "true");
+        properties.setProperty("clearStatementPoolOnReturn", "true");
         properties.setProperty("maxOpenPreparedStatements", "10");
         properties.setProperty("lifo", "true");
         properties.setProperty("fastFailValidation", "true");
@@ -180,6 +181,7 @@
         assertTrue(ds.getLogAbandoned());
         assertTrue(ds.getAbandonedUsageTracking());
         assertTrue(ds.isPoolPreparedStatements());
+        assertTrue(ds.isClearStatementPoolOnReturn());
         assertEquals(10, ds.getMaxOpenPreparedStatements());
         assertTrue(ds.getLifo());
         assertTrue(ds.getFastFailValidation());
diff --git a/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceMXBean.java b/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceMXBean.java
index 6576777..f0f3a38 100644
--- a/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceMXBean.java
+++ b/src/test/java/org/apache/commons/dbcp2/TestBasicDataSourceMXBean.java
@@ -42,6 +42,11 @@
         }
 
         @Override
+        public boolean isClearStatementPoolOnReturn() {
+            return false;
+        }
+        
+        @Override
         public boolean isClosed() {
             return false;
         }
diff --git a/src/test/java/org/apache/commons/dbcp2/TestPStmtPoolingBasicDataSource.java b/src/test/java/org/apache/commons/dbcp2/TestPStmtPoolingBasicDataSource.java
index 734ebcc..e2d7e20 100644
--- a/src/test/java/org/apache/commons/dbcp2/TestPStmtPoolingBasicDataSource.java
+++ b/src/test/java/org/apache/commons/dbcp2/TestPStmtPoolingBasicDataSource.java
@@ -29,6 +29,7 @@
 import java.sql.SQLException;
 import java.sql.Statement;
 
+import org.apache.commons.pool2.KeyedObjectPool;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -234,6 +235,67 @@
     }
 
     /**
+     * Tests clearStatementPoolOnReturn introduced with DBCP-566.
+     * When turned on, the statement pool must be empty after the connection is closed.
+     *  
+     * @throws Exception
+     * @since 2.8.0
+     */
+    @Test
+    public void testPStmtPoolingAcrossCloseWithClearOnReturn() throws Exception {
+        ds.setMaxTotal(1); // only one connection in pool needed
+        ds.setMaxIdle(1);
+        ds.setClearStatementPoolOnReturn(true);
+        ds.setAccessToUnderlyingConnectionAllowed(true);
+        final Connection conn1 = getConnection();
+        assertNotNull(conn1);
+        assertEquals(1, ds.getNumActive());
+        assertEquals(0, ds.getNumIdle());
+
+        @SuppressWarnings("unchecked")
+        final DelegatingConnection<Connection> poolableConn = 
+            (DelegatingConnection<Connection>) ((DelegatingConnection<Connection>) conn1).getDelegateInternal();
+        final KeyedObjectPool<PStmtKey, DelegatingPreparedStatement> stmtPool = 
+            ((PoolingConnection) poolableConn.getDelegateInternal()).getStatementPool();
+
+        final PreparedStatement stmt1 = conn1.prepareStatement("select 'a' from dual");
+        assertNotNull(stmt1);
+        final Statement inner1 = ((DelegatingPreparedStatement) stmt1).getInnermostDelegate();
+        assertNotNull(inner1);
+        stmt1.close();
+        
+        final PreparedStatement stmt2 = conn1.prepareStatement("select 'a' from dual");
+        assertNotNull(stmt2);
+        final Statement inner2 = ((DelegatingPreparedStatement) stmt2).getInnermostDelegate();
+        assertNotNull(inner2);
+        assertSame(inner1, inner2); // from the same connection the statement must be pooled
+        stmt2.close();
+        
+        conn1.close();
+        assertTrue(inner1.isClosed());
+        
+        assertEquals(0, stmtPool.getNumActive());
+        assertEquals(0, stmtPool.getNumIdle());
+
+        assertEquals(0, ds.getNumActive());
+        assertEquals(1, ds.getNumIdle());
+
+        final Connection conn2 = getConnection();
+        assertNotNull(conn2);
+        assertEquals(1, ds.getNumActive());
+        assertEquals(0, ds.getNumIdle());
+
+        final PreparedStatement stmt3 = conn2.prepareStatement("select 'a' from dual");
+        assertNotNull(stmt3);
+        final Statement inner3 = ((DelegatingPreparedStatement) stmt3).getInnermostDelegate();
+        assertNotNull(inner3);
+
+        assertNotSame(inner1, inner3); // when acquiring the connection the next time, statement must be new
+        
+        conn2.close();
+    }
+
+    /**
      * Tests high-concurrency contention for connections and pooled prepared statements.
      * DBCP-414
      */