Skip to content

Commit db1e358

Browse files
committed
Fix ClassCastException with pooled connections in DefaultOutputBuffer
Fixes #110 Connection pools such as HikariCP return proxy statements that are not instances of OracleCallableStatement, so fetching reporter output with a pooled connection failed in DefaultOutputBuffer.getLinesCursorStatement. The cast was never needed, because only standard CallableStatement methods are used. It is replaced with a plain CallableStatement, the same way CompatibilityOutputBufferPre310 already does it. Also close the statement created in Reporter.initDbReporter. It was never closed, so every Reporter.init leaked an open cursor. On long-lived pooled connections this can lead to ORA-01000. Add unit tests for both cases using mocked, non-Oracle JDBC objects.
1 parent f443528 commit db1e358

4 files changed

Lines changed: 112 additions & 7 deletions

File tree

‎src/main/java/org/utplsql/api/outputBuffer/DefaultOutputBuffer.java‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.utplsql.api.outputBuffer;
22

3-
import oracle.jdbc.OracleCallableStatement;
43
import oracle.jdbc.OracleTypes;
54
import org.utplsql.api.reporter.Reporter;
65

@@ -34,7 +33,7 @@ protected CallableStatement getLinesCursorStatement(Connection conn) throws SQLE
3433
" l_rep.set_reporter_id(:reporter_id); " +
3534
" :lines_cursor := l_rep.get_lines_cursor(); " +
3635
"end;";
37-
OracleCallableStatement cstmt = (OracleCallableStatement) conn.prepareCall(plsql);
36+
CallableStatement cstmt = conn.prepareCall(plsql);
3837
cstmt.setString("reporter_id", reporter.getId());
3938
cstmt.registerOutParameter("lines_cursor", OracleTypes.CURSOR);
4039
return cstmt;

‎src/main/java/org/utplsql/api/reporter/Reporter.java‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,14 @@ public Reporter init(Connection con) throws SQLException {
6767
* @throws SQLException if there are problems with the database access
6868
*/
6969
private void initDbReporter(OracleConnection oraConn, ReporterFactory reporterFactory) throws SQLException {
70-
OracleCallableStatement callableStatement = (OracleCallableStatement) oraConn.prepareCall("{? = call " + selfType + "()}");
71-
callableStatement.registerOutParameter(1, OracleTypes.STRUCT, "UT_REPORTER_BASE");
72-
callableStatement.execute();
70+
try (OracleCallableStatement callableStatement = (OracleCallableStatement) oraConn.prepareCall("{? = call " + selfType + "()}")) {
71+
callableStatement.registerOutParameter(1, OracleTypes.STRUCT, "UT_REPORTER_BASE");
72+
callableStatement.execute();
7373

74-
Reporter obj = (Reporter) callableStatement.getORAData(1, reporterFactory);
74+
Reporter obj = (Reporter) callableStatement.getORAData(1, reporterFactory);
7575

76-
setAttributes(obj.getAttributes());
76+
setAttributes(obj.getAttributes());
77+
}
7778

7879
logger.debug("Database-reporter initialized, Type: {}, ID: {}", selfType, id);
7980
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.utplsql.api.outputBuffer;
2+
3+
import oracle.jdbc.OracleTypes;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.Test;
6+
import org.utplsql.api.reporter.Reporter;
7+
8+
import java.sql.CallableStatement;
9+
import java.sql.Connection;
10+
import java.sql.ResultSet;
11+
import java.sql.SQLException;
12+
import java.util.List;
13+
14+
import static org.hamcrest.MatcherAssert.assertThat;
15+
import static org.hamcrest.Matchers.contains;
16+
import static org.mockito.ArgumentMatchers.anyString;
17+
import static org.mockito.Mockito.*;
18+
19+
/**
20+
* Connection pools (e.g. HikariCP) return their own proxy statements, which are not
21+
* {@link oracle.jdbc.OracleCallableStatement} instances. The output buffer must work with plain JDBC statements.
22+
*/
23+
class DefaultOutputBufferTest {
24+
25+
private Reporter reporter;
26+
private Connection connection;
27+
private CallableStatement callableStatement;
28+
29+
@BeforeEach
30+
void setUp() throws SQLException {
31+
reporter = mock(Reporter.class);
32+
when(reporter.isInit()).thenReturn(true);
33+
when(reporter.getTypeName()).thenReturn("UT_DOCUMENTATION_REPORTER");
34+
when(reporter.getId()).thenReturn("ABC123");
35+
36+
ResultSet resultSet = mock(ResultSet.class);
37+
when(resultSet.next()).thenReturn(true, true, false);
38+
when(resultSet.getString("text")).thenReturn("line 1", "line 2");
39+
40+
// A plain CallableStatement, as returned by a connection pool proxy
41+
callableStatement = mock(CallableStatement.class);
42+
when(callableStatement.getObject("lines_cursor")).thenReturn(resultSet);
43+
44+
connection = mock(Connection.class);
45+
when(connection.prepareCall(anyString())).thenReturn(callableStatement);
46+
}
47+
48+
@Test
49+
void fetchAllWorksWithNonOracleCallableStatement() throws SQLException {
50+
List<String> lines = new DefaultOutputBuffer(reporter).fetchAll(connection);
51+
52+
assertThat(lines, contains("line 1", "line 2"));
53+
verify(callableStatement).setString("reporter_id", "ABC123");
54+
verify(callableStatement).registerOutParameter("lines_cursor", OracleTypes.CURSOR);
55+
verify(callableStatement).close();
56+
}
57+
58+
@Test
59+
void printAvailableWorksWithNonOracleCallableStatement() throws SQLException {
60+
new DefaultOutputBuffer(reporter).setFetchSize(1).printAvailable(connection, mock(java.io.PrintStream.class));
61+
62+
verify(callableStatement).setFetchSize(1);
63+
verify(callableStatement).execute();
64+
verify(callableStatement).close();
65+
}
66+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package org.utplsql.api.reporter;
2+
3+
import oracle.jdbc.OracleCallableStatement;
4+
import oracle.jdbc.OracleConnection;
5+
import org.junit.jupiter.api.Test;
6+
import org.utplsql.api.compatibility.CompatibilityProxy;
7+
8+
import java.sql.Connection;
9+
import java.sql.SQLException;
10+
11+
import static org.hamcrest.MatcherAssert.assertThat;
12+
import static org.hamcrest.Matchers.equalTo;
13+
import static org.mockito.ArgumentMatchers.*;
14+
import static org.mockito.Mockito.*;
15+
16+
class ReporterInitTest {
17+
18+
@Test
19+
void initClosesReporterStatement() throws SQLException {
20+
OracleCallableStatement callableStatement = mock(OracleCallableStatement.class);
21+
Reporter dbReporter = new DefaultReporter("UT_DOCUMENTATION_REPORTER",
22+
new Object[]{"UT_DOCUMENTATION_REPORTER", new byte[]{0x0A, 0x0B}});
23+
when(callableStatement.getORAData(eq(1), any())).thenReturn(dbReporter);
24+
25+
OracleConnection oracleConnection = mock(OracleConnection.class);
26+
when(oracleConnection.prepareCall(anyString())).thenReturn(callableStatement);
27+
28+
// Connection as handed out by a connection pool
29+
Connection connection = mock(Connection.class);
30+
when(connection.unwrap(OracleConnection.class)).thenReturn(oracleConnection);
31+
32+
Reporter reporter = new DefaultReporter("UT_DOCUMENTATION_REPORTER", null)
33+
.init(connection, mock(CompatibilityProxy.class), new ReporterFactory());
34+
35+
assertThat(reporter.isInit(), equalTo(true));
36+
assertThat(reporter.getId(), equalTo("0A0B"));
37+
verify(callableStatement).close();
38+
}
39+
}

0 commit comments

Comments
 (0)