Skip to content

Commit 12d24f2

Browse files
authored
Merge pull request #111 from utPLSQL/bugfix/issue_110_pooled_connection_cast
Fix ClassCastException with pooled connections in DefaultOutputBuffer
2 parents f443528 + d623cff commit 12d24f2

6 files changed

Lines changed: 149 additions & 16 deletions

File tree

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

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

3+
import oracle.jdbc.OracleTypes;
34
import org.utplsql.api.reporter.Reporter;
45

56
import java.io.PrintStream;
@@ -79,6 +80,27 @@ public void printAvailable(Connection conn, List<PrintStream> printStreams) thro
7980

8081
protected abstract CallableStatement getLinesCursorStatement(Connection conn) throws SQLException;
8182

83+
/**
84+
* Prepares the given PL/SQL block and binds the :reporter_id and :lines_cursor parameters.
85+
* The returned statement must be closed by the caller. It is closed here if binding fails.
86+
*
87+
* @param conn DB connection
88+
* @param plsql PL/SQL block using the :reporter_id and :lines_cursor bind variables
89+
* @return the prepared statement
90+
* @throws SQLException any sql errors
91+
*/
92+
protected CallableStatement prepareLinesCursorStatement(Connection conn, String plsql) throws SQLException {
93+
CallableStatement cstmt = conn.prepareCall(plsql);
94+
try {
95+
cstmt.setString("reporter_id", reporter.getId());
96+
cstmt.registerOutParameter("lines_cursor", OracleTypes.CURSOR);
97+
return cstmt;
98+
} catch (SQLException | RuntimeException e) {
99+
cstmt.close();
100+
throw e;
101+
}
102+
}
103+
82104
/**
83105
* Print the lines as soon as they are produced and call the callback passing the new line.
84106
*

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

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

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

65
import java.sql.CallableStatement;
@@ -20,9 +19,6 @@ class CompatibilityOutputBufferPre310 extends AbstractOutputBuffer {
2019

2120
@Override
2221
protected CallableStatement getLinesCursorStatement(Connection conn) throws SQLException {
23-
CallableStatement cstmt = conn.prepareCall("begin :lines_cursor := ut_output_buffer.get_lines_cursor(:reporter_id); end;");
24-
cstmt.registerOutParameter("lines_cursor", OracleTypes.CURSOR);
25-
cstmt.setString("reporter_id", getReporter().getId());
26-
return cstmt;
22+
return prepareLinesCursorStatement(conn, "begin :lines_cursor := ut_output_buffer.get_lines_cursor(:reporter_id); end;");
2723
}
2824
}

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

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

3-
import oracle.jdbc.OracleCallableStatement;
4-
import oracle.jdbc.OracleTypes;
53
import org.utplsql.api.reporter.Reporter;
64

75
import java.sql.CallableStatement;
@@ -34,9 +32,6 @@ protected CallableStatement getLinesCursorStatement(Connection conn) throws SQLE
3432
" l_rep.set_reporter_id(:reporter_id); " +
3533
" :lines_cursor := l_rep.get_lines_cursor(); " +
3634
"end;";
37-
OracleCallableStatement cstmt = (OracleCallableStatement) conn.prepareCall(plsql);
38-
cstmt.setString("reporter_id", reporter.getId());
39-
cstmt.registerOutParameter("lines_cursor", OracleTypes.CURSOR);
40-
return cstmt;
35+
return prepareLinesCursorStatement(conn, plsql);
4136
}
4237
}

‎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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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.hamcrest.Matchers.sameInstance;
17+
import static org.junit.jupiter.api.Assertions.assertThrows;
18+
import static org.mockito.ArgumentMatchers.anyString;
19+
import static org.mockito.Mockito.*;
20+
21+
/**
22+
* Connection pools (e.g. HikariCP) return their own proxy statements, which are not
23+
* {@link oracle.jdbc.OracleCallableStatement} instances. The output buffer must work with plain JDBC statements.
24+
*/
25+
class DefaultOutputBufferTest {
26+
27+
private Reporter reporter;
28+
private Connection connection;
29+
private CallableStatement callableStatement;
30+
31+
@BeforeEach
32+
void setUp() throws SQLException {
33+
reporter = mock(Reporter.class);
34+
when(reporter.isInit()).thenReturn(true);
35+
when(reporter.getTypeName()).thenReturn("UT_DOCUMENTATION_REPORTER");
36+
when(reporter.getId()).thenReturn("ABC123");
37+
38+
ResultSet resultSet = mock(ResultSet.class);
39+
when(resultSet.next()).thenReturn(true, true, false);
40+
when(resultSet.getString("text")).thenReturn("line 1", "line 2");
41+
42+
// A plain CallableStatement, as returned by a connection pool proxy
43+
callableStatement = mock(CallableStatement.class);
44+
when(callableStatement.getObject("lines_cursor")).thenReturn(resultSet);
45+
46+
connection = mock(Connection.class);
47+
when(connection.prepareCall(anyString())).thenReturn(callableStatement);
48+
}
49+
50+
@Test
51+
void fetchAllWorksWithNonOracleCallableStatement() throws SQLException {
52+
List<String> lines = new DefaultOutputBuffer(reporter).fetchAll(connection);
53+
54+
assertThat(lines, contains("line 1", "line 2"));
55+
verify(callableStatement).setString("reporter_id", "ABC123");
56+
verify(callableStatement).registerOutParameter("lines_cursor", OracleTypes.CURSOR);
57+
verify(callableStatement).close();
58+
}
59+
60+
@Test
61+
void printAvailableWorksWithNonOracleCallableStatement() throws SQLException {
62+
new DefaultOutputBuffer(reporter).setFetchSize(1).printAvailable(connection, mock(java.io.PrintStream.class));
63+
64+
verify(callableStatement).setFetchSize(1);
65+
verify(callableStatement).execute();
66+
verify(callableStatement).close();
67+
}
68+
69+
@Test
70+
void statementIsClosedWhenBindingFails() throws SQLException {
71+
SQLException bindError = new SQLException("bind failed");
72+
doThrow(bindError).when(callableStatement).setString(anyString(), anyString());
73+
74+
SQLException thrown = assertThrows(SQLException.class, () -> new DefaultOutputBuffer(reporter).fetchAll(connection));
75+
76+
assertThat(thrown, sameInstance(bindError));
77+
verify(callableStatement).close();
78+
verify(callableStatement, never()).execute();
79+
}
80+
}
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)