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

markt pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit aa0a9932f0d8813a1ab1cb3597fa79e02acf24ee
Author: Michael Clarke <mcla...@apache.org>
AuthorDate: Thu Jul 18 17:05:57 2024 +0100

    Correctly Proxy Statement returned from ResultSet
    
    The tomcat-jdbc connection pool creates reflective proxies around
    Connections, Statements, and ResultSets to allow interception of 'close'
    calls, and consistency of traversing the link back from a child element
    to its parent - e.g. Statement to Connection. However, the link from
    ResultSet to Statement does not intercept the call so a non-proxied
    Statement is returned, as well as the `equals` method not handling the
    comparison against a non-proxied class correctly. To overcome this, an
    invocation handler is being created for wrapping a ResultSet returned
    from any of the relevant Statement methods, with that invocation handler
    intercepting the `getStatement` call and returning the proxied creating
    statement. The `equals` checks have also been altered to handle
    non-proxied instances being passed in to them.
---
 .../apache/tomcat/jdbc/pool/StatementFacade.java   |  96 ++++++++++
 .../AbstractCreateStatementInterceptor.java        |  21 ++-
 .../tomcat/jdbc/test/ProxiedResultSetTest.java     | 193 +++++++++++++++++++++
 .../tomcat/jdbc/test/ProxiedStatementTest.java     |  61 +++++++
 .../apache/tomcat/jdbc/test/driver/ResultSet.java  |  11 +-
 .../apache/tomcat/jdbc/test/driver/Statement.java  |   8 +-
 6 files changed, 382 insertions(+), 8 deletions(-)

diff --git 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/StatementFacade.java
 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/StatementFacade.java
index 4771b321b4..e30f7d91ec 100644
--- 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/StatementFacade.java
+++ 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/StatementFacade.java
@@ -23,6 +23,7 @@ import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 import java.sql.CallableStatement;
 import java.sql.PreparedStatement;
+import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
 
@@ -93,6 +94,9 @@ public class StatementFacade extends 
AbstractCreateStatementInterceptor {
                 return toString();
             }
             if (compare(EQUALS_VAL, method)) {
+                if (args[0] == null || 
!Proxy.isProxyClass(args[0].getClass())) {
+                    return Boolean.FALSE;
+                }
                 return Boolean.valueOf(
                         this.equals(Proxy.getInvocationHandler(args[0])));
             }
@@ -112,6 +116,17 @@ public class StatementFacade extends 
AbstractCreateStatementInterceptor {
             if (delegate == null) {
               throw new SQLException("Statement closed.");
             }
+
+            if (compare(GET_RESULTSET, method)) {
+                return getConstructor(RESULTSET_IDX, 
ResultSet.class).newInstance(new ResultSetProxy(method.invoke(delegate,args), 
proxy));
+            }
+            if (compare(GET_GENERATED_KEYS, method)) {
+                return getConstructor(RESULTSET_IDX, 
ResultSet.class).newInstance(new ResultSetProxy(method.invoke(delegate,args), 
proxy));
+            }
+            if (compare(EXECUTE_QUERY, method)) {
+                return getConstructor(RESULTSET_IDX, 
ResultSet.class).newInstance(new ResultSetProxy(method.invoke(delegate,args), 
proxy));
+            }
+
             Object result =  null;
             try {
                 //invoke next
@@ -154,4 +169,85 @@ public class StatementFacade extends 
AbstractCreateStatementInterceptor {
         }
     }
 
+    protected class ResultSetProxy implements InvocationHandler {
+
+        private final Object parent;
+        private Object delegate;
+
+        public ResultSetProxy(Object delegate, Object parent) {
+            this.delegate = delegate;
+            this.parent = parent;
+        }
+
+        @Override
+        public Object invoke(Object proxy, Method method, Object[] args) 
throws Throwable {
+            if (compare(TOSTRING_VAL,method)) {
+                return toString();
+            }
+            if (compare(EQUALS_VAL, method)) {
+                if (args[0] == null || 
!Proxy.isProxyClass(args[0].getClass())) {
+                    return Boolean.FALSE;
+                }
+                return Boolean.valueOf(
+                    this.equals(Proxy.getInvocationHandler(args[0])));
+            }
+            if (compare(HASHCODE_VAL, method)) {
+                return Integer.valueOf(this.hashCode());
+            }
+            if (compare(CLOSE_VAL, method)) {
+                if (delegate == null) {
+                    return null;
+                }
+            }
+            if (compare(ISCLOSED_VAL, method)) {
+                if (delegate == null) {
+                    return Boolean.TRUE;
+                }
+            }
+            if (delegate == null) {
+                throw new SQLException("ResultSet closed.");
+            }
+
+            if (compare(GET_STATEMENT, method)) {
+                return parent;
+            }
+
+            Object result;
+            try {
+                //invoke next
+                result =  method.invoke(delegate,args);
+            } catch (Throwable t) {
+                if (t instanceof InvocationTargetException && t.getCause() != 
null) {
+                    throw t.getCause();
+                } else {
+                    throw t;
+                }
+            }
+            //perform close cleanup
+            if (compare(CLOSE_VAL, method)) {
+                delegate = null;
+            }
+            return result;
+        }
+
+        @Override
+        public int hashCode() {
+            return System.identityHashCode(this);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            return this==obj;
+        }
+
+        @Override
+        public String toString() {
+            return ResultSetProxy.class.getName() + "[Proxy=" +
+                hashCode() +
+                "; Delegate=" +
+                delegate +
+                ']';
+        }
+    }
+
 }
diff --git 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java
 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java
index 9e64a931da..8ce289b577 100644
--- 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java
+++ 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/interceptor/AbstractCreateStatementInterceptor.java
@@ -20,6 +20,8 @@ import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
+import java.sql.ResultSet;
+import java.sql.Statement;
 
 import org.apache.tomcat.jdbc.pool.ConnectionPool;
 import org.apache.tomcat.jdbc.pool.JdbcInterceptor;
@@ -38,6 +40,23 @@ public abstract class  AbstractCreateStatementInterceptor 
extends JdbcIntercepto
     protected static final String PREPARE_CALL          = "prepareCall";
     protected static final int    PREPARE_CALL_IDX      = 2;
 
+    /**
+     * {@link Statement#getResultSet()}
+     */
+    protected static final String GET_RESULTSET = "getResultSet";
+
+    /**
+     * {@link Statement#getGeneratedKeys()}
+     */
+    protected static final String GET_GENERATED_KEYS = "getGeneratedKeys";
+
+    /**
+     * {@link ResultSet#getStatement()}
+     */
+    protected static final String GET_STATEMENT = "getStatement";
+
+    protected static final int    RESULTSET_IDX         = 3;
+
     protected static final String[] STATEMENT_TYPES = {CREATE_STATEMENT, 
PREPARE_STATEMENT, PREPARE_CALL};
     protected static final int    STATEMENT_TYPE_COUNT = 
STATEMENT_TYPES.length;
 
@@ -51,7 +70,7 @@ public abstract class  AbstractCreateStatementInterceptor 
extends JdbcIntercepto
     /**
      * the constructors that are used to create statement proxies
      */
-    protected static final Constructor<?>[] constructors = new 
Constructor[STATEMENT_TYPE_COUNT];
+    protected static final Constructor<?>[] constructors = new 
Constructor[STATEMENT_TYPE_COUNT + 1];
 
     public  AbstractCreateStatementInterceptor() {
         super();
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/ProxiedResultSetTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/ProxiedResultSetTest.java
new file mode 100644
index 0000000000..ae4d951f2c
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/ProxiedResultSetTest.java
@@ -0,0 +1,193 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomcat.jdbc.test;
+
+import org.apache.tomcat.jdbc.test.driver.Driver;
+import org.junit.Test;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+public class ProxiedResultSetTest extends DefaultTestCase {
+
+    @Test
+    public void 
shouldReturnWrappedOwningPreparedStatementFromExecuteQueryResultSet() throws 
Exception {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             PreparedStatement statement = con.prepareStatement("");
+             ResultSet resultSet = statement.executeQuery()) {
+            assertEquals(statement, resultSet.getStatement());
+        }
+    }
+
+    @Test
+    public void 
shouldReturnWrappedOwningStatementFromGetGeneratedKeyResultSet() throws 
Exception {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             Statement statement = 
con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
+             ResultSet resultSet = statement.getGeneratedKeys()) {
+            assertEquals(statement, resultSet.getStatement());
+        }
+    }
+
+    @Test
+    public void shouldReturnWrappedOwningStatementFromExecuteQuery() throws 
Exception {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             Statement statement = con.createStatement();
+             ResultSet resultSet = statement.executeQuery("")) {
+            assertEquals(statement, resultSet.getStatement());
+        }
+    }
+
+    @Test
+    public void shouldReturnWrappedOwningStatementForGetResultSet() throws 
Exception {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             Statement statement = con.createStatement();
+             ResultSet resultSet = statement.getResultSet()) {
+            assertEquals(statement, resultSet.getStatement());
+        }
+    }
+
+    @Test
+    public void shouldReturnCorrectClosedStatus() throws SQLException {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = datasource.getConnection();
+             Statement statement = con.createStatement();
+             ResultSet resultSet = statement.getResultSet()) {
+            assertFalse(resultSet.isClosed());
+            resultSet.close();
+            assertTrue(resultSet.isClosed());
+        }
+    }
+
+    @Test
+    public void shouldReturnHashcodeRegardlessOfClosedStatus() throws 
SQLException {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = datasource.getConnection();
+             Statement statement = con.createStatement();
+             ResultSet resultSet = statement.getResultSet()) {
+            int hashcode = resultSet.hashCode();
+            resultSet.close();
+            assertEquals(hashcode, resultSet.hashCode());
+        }
+    }
+
+    @Test
+    public void shouldReturnEqualsRegardlessOfClosedStatus() throws 
SQLException {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = datasource.getConnection();
+             Statement statement = con.createStatement();
+             ResultSet resultSet = statement.getResultSet()) {
+            assertNotEquals(resultSet, "");
+            assertEquals(resultSet, resultSet);
+            resultSet.close();
+            assertNotEquals(resultSet, "");
+            assertEquals(resultSet, resultSet);
+        }
+    }
+
+
+    @Test
+    public void shouldReturnToStringRegardlessOfClosedStatus() throws 
SQLException {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = datasource.getConnection();
+             Statement statement = con.createStatement();
+             ResultSet resultSet = statement.getResultSet()) {
+            String toStringResult = resultSet.toString();
+            resultSet.close();
+            // the delegate will change, so we can't compare the whole string
+            assertEquals(toStringResult.substring(0, 50), 
resultSet.toString().substring(0, 50));
+        }
+    }
+
+
+    @Test
+    public void shouldReturnFalseForNullEqualsComparison() throws Exception {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             PreparedStatement statement = con.prepareStatement("sql");
+             ResultSet resultSet = statement.executeQuery()) {
+            assertNotEquals(resultSet, null);
+        }
+    }
+
+    @Test
+    public void shouldReturnFalseForDifferentObjectTypeEqualsComparison() 
throws Exception {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             PreparedStatement statement = con.prepareStatement("sql");
+             ResultSet resultSet = statement.executeQuery()) {
+            assertNotEquals(resultSet, "");
+        }
+    }
+
+    @Test
+    public void shouldReturnFalseForNonProxiedResultSetEqualsComparison() 
throws Exception {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             PreparedStatement statement = con.prepareStatement("sql");
+             ResultSet resultSet = statement.executeQuery()) {
+            assertNotEquals(resultSet, new 
org.apache.tomcat.jdbc.test.driver.ResultSet(statement));
+        }
+    }
+
+    @Test
+    public void shouldReturnTrueForSameProxiedResultSetEqualsComparison() 
throws Exception {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             PreparedStatement statement = con.prepareStatement("sql");
+             ResultSet resultSet = statement.executeQuery()) {
+            assertEquals(resultSet, resultSet);
+        }
+    }
+
+    @Test
+    public void shouldReturnFalseForNonEqualProxiedResultSetEqualsComparison() 
throws Exception {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             PreparedStatement statement = con.prepareStatement("sql");
+             ResultSet resultSet = statement.executeQuery();
+             Connection con2 = this.datasource.getConnection();
+             PreparedStatement statement2 = con2.prepareStatement("sql");
+             ResultSet resultSet2 = statement2.executeQuery()) {
+            assertNotEquals(resultSet, resultSet2);
+        }
+    }
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/ProxiedStatementTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/ProxiedStatementTest.java
new file mode 100644
index 0000000000..c7b16b3125
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/ProxiedStatementTest.java
@@ -0,0 +1,61 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.apache.tomcat.jdbc.test;
+
+import org.apache.tomcat.jdbc.test.driver.Driver;
+import org.junit.Test;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import static org.junit.Assert.assertNotEquals;
+
+public class ProxiedStatementTest extends DefaultTestCase {
+
+    @Test
+    public void shouldReturnFalseForNullEqualsComparison() throws SQLException 
{
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             PreparedStatement statement = con.prepareStatement("sql")) {
+            assertNotEquals(statement, null);
+        }
+    }
+
+    @Test
+    public void shouldReturnFalseForDifferentObjectTypeEqualsComparison() 
throws SQLException {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             Statement statement = 
con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
+            assertNotEquals(statement, "");
+        }
+    }
+
+    @Test
+    public void shouldReturnFalseForNonProxiedResultSetEqualsComparison() 
throws SQLException {
+        this.datasource.setDriverClassName(Driver.class.getName());
+        this.datasource.setUrl("jdbc:tomcat:test");
+        try (Connection con = this.datasource.getConnection();
+             Statement statement = 
con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
+            assertNotEquals(statement, new 
org.apache.tomcat.jdbc.test.driver.Statement());
+        }
+    }
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/ResultSet.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/ResultSet.java
index 71d26b8d75..86ce153b7a 100644
--- 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/ResultSet.java
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/ResultSet.java
@@ -38,7 +38,13 @@ import java.util.Calendar;
 import java.util.Map;
 
 public class ResultSet implements java.sql.ResultSet {
-    boolean hasNext = true;
+
+    private final Statement owner;
+    private boolean hasNext = true;
+
+    public ResultSet(Statement owner) {
+        this.owner = owner;
+    }
 
     @Override
     public boolean absolute(int row) throws SQLException {
@@ -437,7 +443,7 @@ public class ResultSet implements java.sql.ResultSet {
     @Override
     public Statement getStatement() throws SQLException {
         // TODO Auto-generated method stub
-        return null;
+        return owner;
     }
 
     @Override
@@ -1219,5 +1225,4 @@ public class ResultSet implements java.sql.ResultSet {
         return null;
     }
 
-
 }
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Statement.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Statement.java
index c37bb086b4..1d48adcc7e 100644
--- 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Statement.java
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/test/driver/Statement.java
@@ -729,7 +729,7 @@ public class Statement implements CallableStatement {
     @Override
     public ResultSet executeQuery() throws SQLException {
         // TODO Auto-generated method stub
-        return new org.apache.tomcat.jdbc.test.driver.ResultSet();
+        return new org.apache.tomcat.jdbc.test.driver.ResultSet(this);
     }
 
     @Override
@@ -1101,7 +1101,7 @@ public class Statement implements CallableStatement {
     @Override
     public ResultSet executeQuery(String sql) throws SQLException {
         // TODO Auto-generated method stub
-        return new org.apache.tomcat.jdbc.test.driver.ResultSet();
+        return new org.apache.tomcat.jdbc.test.driver.ResultSet(this);
     }
 
     @Override
@@ -1149,7 +1149,7 @@ public class Statement implements CallableStatement {
     @Override
     public ResultSet getGeneratedKeys() throws SQLException {
         // TODO Auto-generated method stub
-        return new org.apache.tomcat.jdbc.test.driver.ResultSet();
+        return new org.apache.tomcat.jdbc.test.driver.ResultSet(this);
     }
 
     @Override
@@ -1185,7 +1185,7 @@ public class Statement implements CallableStatement {
     @Override
     public ResultSet getResultSet() throws SQLException {
         // TODO Auto-generated method stub
-        return null;
+        return new org.apache.tomcat.jdbc.test.driver.ResultSet(this);
     }
 
     @Override


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to