/** * 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.drill.jdbc.test; import static org.junit.Assert.*; import static org.hamcrest.CoreMatchers.*; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; import org.slf4j.Logger; import static org.slf4j.LoggerFactory.getLogger; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Array; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLClientInfoException; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; import java.sql.Struct; import java.util.ArrayList; import java.util.List; import org.apache.drill.jdbc.Driver; import org.apache.drill.jdbc.JdbcTestBase; import org.apache.drill.jdbc.AlreadyClosedSqlException; /** * Test class for JDBC requirement that almost all methods throw * {@link SQLException} when called on a closed primary object (e.g., * {@code Connection}, {@code ResultSet}, etc.). * <p> * NOTE: This test currently covers: * {@link Connection}, * {@link Statement}, * {@link PreparedStatement}, * {@link ResultSet}, * {@link ResultSetMetadata}, and * {@link DatabaseMetaData}. * </p> * <p> * It does not cover unimplemented {@link CallableStatement} or any relevant * secondary objects such as {@link Array} or {@link Struct}). * </p> */ public class Drill2489CallsAfterCloseThrowExceptionsTest extends JdbcTestBase { private static final Logger logger = getLogger(Drill2489CallsAfterCloseThrowExceptionsTest.class); private static Connection closedConn; private static Connection openConn; private static Statement closedPlainStmtOfOpenConn; private static PreparedStatement closedPreparedStmtOfOpenConn; // No CallableStatement. private static ResultSet closedResultSetOfClosedStmt; private static ResultSet closedResultSetOfOpenStmt; private static ResultSetMetaData resultSetMetaDataOfClosedResultSet; private static ResultSetMetaData resultSetMetaDataOfClosedStmt; private static DatabaseMetaData databaseMetaDataOfClosedConn; @BeforeClass public static void setUpClosedObjects() throws Exception { // (Note: Can't use JdbcTest's connect(...) for this test class.) final Connection connToClose = new Driver().connect("jdbc:drill:zk=local", JdbcAssert.getDefaultProperties()); final Connection connToKeep = new Driver().connect("jdbc:drill:zk=local", JdbcAssert.getDefaultProperties()); final Statement plainStmtToClose = connToKeep.createStatement(); final Statement plainStmtToKeep = connToKeep.createStatement(); final PreparedStatement preparedStmtToClose = connToKeep.prepareStatement("VALUES 'PreparedStatement query'"); try { connToKeep.prepareCall("VALUES 'CallableStatement query'"); fail("Test seems to be out of date. Was prepareCall(...) implemented?"); } catch (SQLException | UnsupportedOperationException e) { // Expected. } final ResultSet resultSetToCloseOnStmtToClose = plainStmtToClose.executeQuery("VALUES 'plain Statement query'"); resultSetToCloseOnStmtToClose.next(); final ResultSet resultSetToCloseOnStmtToKeep = plainStmtToKeep.executeQuery("VALUES 'plain Statement query'"); resultSetToCloseOnStmtToKeep.next(); final ResultSetMetaData rsmdForClosedStmt = resultSetToCloseOnStmtToKeep.getMetaData(); final ResultSetMetaData rsmdForOpenStmt = resultSetToCloseOnStmtToClose.getMetaData(); final DatabaseMetaData dbmd = connToClose.getMetaData(); connToClose.close(); plainStmtToClose.close(); preparedStmtToClose.close(); resultSetToCloseOnStmtToClose.close(); resultSetToCloseOnStmtToKeep.close(); closedConn = connToClose; openConn = connToKeep; closedPlainStmtOfOpenConn = plainStmtToClose; closedPreparedStmtOfOpenConn = preparedStmtToClose; closedResultSetOfClosedStmt = resultSetToCloseOnStmtToClose; closedResultSetOfOpenStmt = resultSetToCloseOnStmtToKeep; resultSetMetaDataOfClosedResultSet = rsmdForOpenStmt; resultSetMetaDataOfClosedStmt = rsmdForClosedStmt; databaseMetaDataOfClosedConn = dbmd; // Self-check that member variables are set (and objects are in right open // or closed state): assertTrue("Test setup error", closedConn.isClosed()); assertFalse("Test setup error", openConn.isClosed()); assertTrue("Test setup error", closedPlainStmtOfOpenConn.isClosed()); assertTrue("Test setup error", closedPreparedStmtOfOpenConn.isClosed()); assertTrue("Test setup error", closedResultSetOfClosedStmt.isClosed()); assertTrue("Test setup error", closedResultSetOfOpenStmt.isClosed()); // (No ResultSetMetaData.isClosed() or DatabaseMetaData.isClosed():) assertNotNull("Test setup error", resultSetMetaDataOfClosedResultSet); assertNotNull("Test setup error", resultSetMetaDataOfClosedStmt); assertNotNull("Test setup error", databaseMetaDataOfClosedConn); } @AfterClass public static void tearDownConnection() throws Exception { openConn.close(); } /////////////////////////////////////////////////////////////// // 1. Check that isClosed() and close() do not throw, and isClosed() returns // true. @Test public void testClosedConnection_close_doesNotThrow() throws SQLException { closedConn.close(); } @Test public void testClosedConnection_isClosed_returnsTrue() throws SQLException { assertThat(closedConn.isClosed(), equalTo(true)); } @Test public void testClosedPlainStatement_close_doesNotThrow() throws SQLException { closedPlainStmtOfOpenConn.close(); } @Test public void testClosedPlainStatement_isClosed_returnsTrue() throws SQLException { assertThat(closedPlainStmtOfOpenConn.isClosed(), equalTo(true)); } @Test public void testClosedPreparedStatement_close_doesNotThrow() throws SQLException { closedPreparedStmtOfOpenConn.close(); } @Test public void testClosedPreparedStatement_isClosed_returnsTrue() throws SQLException { assertThat(closedPreparedStmtOfOpenConn.isClosed(), equalTo(true)); } @Test public void testClosedResultSet_close_doesNotThrow() throws SQLException { closedResultSetOfOpenStmt.close(); } @Test public void testClosedResultSet_isClosed_returnsTrue() throws SQLException { assertThat(closedResultSetOfOpenStmt.isClosed(), equalTo(true)); } /////////////////////////////////////////////////////////////// // 2. Check that all methods throw or not appropriately (either as specified // by JDBC or currently intended as partial Avatica workaround). /** * Reflection-based checker of throwing of "already closed" exception by JDBC * interfaces' implementation methods. * * @param <INTF> JDBC interface type */ private static abstract class ThrowsClosedBulkChecker<INTF> { private final Class<INTF> jdbcIntf; private final INTF jdbcObject; protected final String normalClosedExceptionText; private String methodLabel; // for inter-method multi-return passing private Object[] argsArray; // for inter-method multi-return passing private final StringBuilder failureLinesBuf = new StringBuilder(); private final StringBuilder successLinesBuf = new StringBuilder(); ThrowsClosedBulkChecker(final Class<INTF> jdbcIntf, final INTF jdbcObject, final String normalClosedExceptionText) { this.jdbcIntf = jdbcIntf; this.jdbcObject = jdbcObject; this.normalClosedExceptionText = normalClosedExceptionText; } /** * Gets minimal value suitable for use as actual parameter value for given * formal parameter type. */ private static Object getDummyValueForType(Class<?> type) { final Object result; if (! type.isPrimitive()) { result = null; } else { if (type == boolean.class) { result = false; } else if (type == byte.class) { result = (byte) 0; } else if (type == short.class) { result = (short) 0; } else if (type == char.class) { result = (char) 0; } else if (type == int.class) { result = 0; } else if (type == long.class) { result = (long) 0L; } else if (type == float.class) { result = 0F; } else if (type == double.class) { result = 0.0; } else { fail("Test needs to be updated to handle type " + type); result = null; // Not executed; for "final". } } return result; } /** * Assembles arguments array and method signature text for given method. * Updates members args and methodLabel. */ private void makeArgsAndLabel(Method method) { final List<Object> argsList = new ArrayList<>(); methodLabel = jdbcIntf.getSimpleName() + "." + method.getName() + "("; boolean first = true; for (Class<?> paramType : method.getParameterTypes()) { if (! first) { methodLabel += ", "; } first = false; methodLabel += paramType.getSimpleName(); argsList.add(getDummyValueForType(paramType)); } methodLabel += ")"; argsArray = argsList.toArray(); } /** * Reports whether it's okay if given method didn't throw any exception. */ protected boolean isOkayNonthrowingMethod(Method method) { return "isClosed".equals(method.getName()) || "close".equals(method.getName()); } /** * Reports whether it's okay if given method throw given exception (that is * not preferred AlreadyClosedException with regular message). */ protected boolean isOkaySpecialCaseException(Method method, Throwable cause) { return false; } /** * Tests one method. * (Disturbs members set by makeArgsAndLabel, but those shouldn't be used * except by this method.) */ private void testOneMethod(Method method) { makeArgsAndLabel(method); logger.debug("Testing method " + methodLabel); try { // See if method throws exception: method.invoke(jdbcObject, argsArray); // If here, method didn't throw--check if it's an expected non-throwing // method (e.g., an isClosed). (If not, report error.) final String resultLine = "- " + methodLabel + " didn't throw\n"; if (isOkayNonthrowingMethod(method)) { successLinesBuf.append(resultLine); } else { logger.trace("Failure: " + resultLine); failureLinesBuf.append(resultLine); } } catch (InvocationTargetException e) { final Throwable cause = e.getCause(); final String resultLine = "- " + methodLabel + " threw <" + cause + ">\n"; if (AlreadyClosedSqlException.class == cause.getClass() && normalClosedExceptionText.equals(cause.getMessage())) { // Common good case--our preferred exception class with our message. successLinesBuf.append(resultLine); } else if (NullPointerException.class == cause.getClass() && (method.getName().equals("isWrapperFor") || method.getName().equals("unwrap"))) { // Known good-enough case--these methods don't throw already-closed // exception, but do throw NullPointerException because of the way // we call them (with null) and the way Avatica code implements them. successLinesBuf.append(resultLine); } else { // Not a case that base-class code here recognizes, but subclass may // know that it's okay. if (isOkaySpecialCaseException(method, cause)) { successLinesBuf.append(resultLine); } else { final String badResultLine = "- " + methodLabel + " threw <" + cause + "> instead" + " of " + AlreadyClosedSqlException.class.getSimpleName() + " with \"" + normalClosedExceptionText.replaceAll("\"", "\"\"") + "\"" + "\n"; logger.trace("Failure: " + resultLine); failureLinesBuf.append(badResultLine); } } } catch (IllegalAccessException | IllegalArgumentException e) { fail("Unexpected exception: " + e + ", cause = " + e.getCause() + " from " + method); } } public void testAllMethods() { for (Method method : jdbcIntf.getMethods()) { testOneMethod(method); } } public boolean hadAnyFailures() { return 0 != failureLinesBuf.length(); } public String getFailureLines() { return failureLinesBuf.toString(); } public String getSuccessLines() { return successLinesBuf.toString(); } public String getReport() { final String report = "Failures:\n" + getFailureLines() + "(Successes:\n" + getSuccessLines() + ")"; return report; } } // class ThrowsClosedChecker<INTF> private static class ClosedConnectionChecker extends ThrowsClosedBulkChecker<Connection> { private static final String STATEMENT_CLOSED_MESSAGE = "Connection is already closed."; ClosedConnectionChecker(Class<Connection> intf, Connection jdbcObject) { super(intf, jdbcObject, STATEMENT_CLOSED_MESSAGE); } @Override protected boolean isOkaySpecialCaseException(Method method, Throwable cause) { final boolean result; if (super.isOkaySpecialCaseException(method, cause)) { result = true; } else if (SQLClientInfoException.class == cause.getClass() && normalClosedExceptionText.equals(cause.getMessage()) && (false || method.getName().equals("setClientInfo") || method.getName().equals("getClientInfo") )) { // Special good case--we had to use SQLClientInfoException from those. result = true; } else if (RuntimeException.class == cause.getClass() && normalClosedExceptionText.equals(cause.getMessage()) && (false || method.getName().equals("getCatalog") || method.getName().equals("getSchema") )) { // Special good-enough case--we had to use RuntimeException for now. result = true; } else { result = false; } return result; } } // class ClosedConnectionChecker @Test public void testClosedConnectionMethodsThrowRight() { ThrowsClosedBulkChecker<Connection> checker = new ClosedConnectionChecker(Connection.class, closedConn); checker.testAllMethods(); if (checker.hadAnyFailures()) { System.err.println(checker.getReport()); fail("Already-closed exception error(s): \n" + checker.getReport()); } } private static class ClosedPlainStatementChecker extends ThrowsClosedBulkChecker<Statement> { private static final String PLAIN_STATEMENT_CLOSED_MESSAGE = "Statement is already closed."; ClosedPlainStatementChecker(Class<Statement> intf, Statement jdbcObject) { super(intf, jdbcObject, PLAIN_STATEMENT_CLOSED_MESSAGE); } @Override protected boolean isOkayNonthrowingMethod(Method method) { // TODO: Java 8 method if ("getLargeUpdateCount".equals(method.getName())) { return true; } return super.isOkayNonthrowingMethod(method); } @Override protected boolean isOkaySpecialCaseException(Method method, Throwable cause) { final boolean result; if (super.isOkaySpecialCaseException(method, cause)) { result = true; } else if ( method.getName().equals("executeLargeBatch") || method.getName().equals("executeLargeUpdate")) { // TODO: New Java 8 methods not implemented in Avatica. result = true; } else if (RuntimeException.class == cause.getClass() && normalClosedExceptionText.equals(cause.getMessage()) && (false || method.getName().equals("getConnection") || method.getName().equals("getFetchDirection") || method.getName().equals("getFetchSize") || method.getName().equals("getMaxRows") || method.getName().equals("getLargeMaxRows") // TODO: Java 8 )) { // Special good-enough case--we had to use RuntimeException for now. result = true; } else { result = false; } return result; } } // class ClosedPlainStatementChecker @Test public void testClosedPlainStatementMethodsThrowRight() { ThrowsClosedBulkChecker<Statement> checker = new ClosedPlainStatementChecker(Statement.class, closedPlainStmtOfOpenConn); checker.testAllMethods(); if (checker.hadAnyFailures()) { fail("Already-closed exception error(s): \n" + checker.getReport()); } } private static class ClosedPreparedStatementChecker extends ThrowsClosedBulkChecker<PreparedStatement> { private static final String PREPAREDSTATEMENT_CLOSED_MESSAGE = "PreparedStatement is already closed."; ClosedPreparedStatementChecker(Class<PreparedStatement> intf, PreparedStatement jdbcObject) { super(intf, jdbcObject, PREPAREDSTATEMENT_CLOSED_MESSAGE); } @Override protected boolean isOkayNonthrowingMethod(Method method) { // TODO: Java 8 methods not yet supported by Avatica. if (method.getName().equals("getLargeUpdateCount")) { return true; } return super.isOkayNonthrowingMethod(method); } @Override protected boolean isOkaySpecialCaseException(Method method, Throwable cause) { final boolean result; if (super.isOkaySpecialCaseException(method, cause)) { result = true; } else if (RuntimeException.class == cause.getClass() && normalClosedExceptionText.equals(cause.getMessage()) && (false || method.getName().equals("getConnection") || method.getName().equals("getFetchDirection") || method.getName().equals("getFetchSize") || method.getName().equals("getMaxRows") || method.getName().equals("getMetaData") )) { // Special good-enough case--we had to use RuntimeException for now. result = true; } else if ( method.getName().equals("setObject") || method.getName().equals("executeLargeUpdate") || method.getName().equals("executeLargeBatch") || method.getName().equals("getLargeMaxRows") ) { // TODO: Java 8 methods not yet supported by Avatica. result = true; } else { result = false; } return result; } } // class closedPreparedStmtOfOpenConnChecker @Test public void testclosedPreparedStmtOfOpenConnMethodsThrowRight() { ThrowsClosedBulkChecker<PreparedStatement> checker = new ClosedPreparedStatementChecker(PreparedStatement.class, closedPreparedStmtOfOpenConn); checker.testAllMethods(); if (checker.hadAnyFailures()) { fail("Already-closed exception error(s): \n" + checker.getReport()); } } private static class ClosedResultSetChecker extends ThrowsClosedBulkChecker<ResultSet> { private static final String RESULTSET_CLOSED_MESSAGE = "ResultSet is already closed."; ClosedResultSetChecker(Class<ResultSet> intf, ResultSet jdbcObject) { super(intf, jdbcObject, RESULTSET_CLOSED_MESSAGE); } @Override protected boolean isOkaySpecialCaseException(Method method, Throwable cause) { final boolean result; if (super.isOkaySpecialCaseException(method, cause)) { result = true; } else if (RuntimeException.class == cause.getClass() && normalClosedExceptionText.equals(cause.getMessage()) && method.getName().equals("getStatement")) { // Special good-enough case--we had to use RuntimeException for now. result = true; } else if (SQLFeatureNotSupportedException.class == cause.getClass() && (method.getName().equals("updateObject"))) { // TODO: Java 8 methods not yet supported by Avatica. result = true; } else { result = false; } return result; } } // class ClosedResultSetChecker @Test public void testClosedResultSetMethodsThrowRight1() { ThrowsClosedBulkChecker<ResultSet> checker = new ClosedResultSetChecker(ResultSet.class, closedResultSetOfClosedStmt); checker.testAllMethods(); if (checker.hadAnyFailures()) { fail("Already-closed exception error(s): \n" + checker.getReport()); } } @Test public void testClosedResultSetMethodsThrowRight2() { ThrowsClosedBulkChecker<ResultSet> checker = new ClosedResultSetChecker(ResultSet.class, closedResultSetOfOpenStmt); checker.testAllMethods(); if (checker.hadAnyFailures()) { fail("Already-closed exception error(s): \n" + checker.getReport()); } } private static class ClosedResultSetMetaDataChecker extends ThrowsClosedBulkChecker<ResultSetMetaData> { private static final String RESULTSETMETADATA_CLOSED_MESSAGE = "ResultSetMetaData's ResultSet is already closed."; ClosedResultSetMetaDataChecker(Class<ResultSetMetaData> intf, ResultSetMetaData jdbcObject) { super(intf, jdbcObject, RESULTSETMETADATA_CLOSED_MESSAGE); } } // class ClosedResultSetMetaDataChecker @Test public void testClosedResultSetMetaDataMethodsThrowRight1() { ThrowsClosedBulkChecker<ResultSetMetaData> checker = new ClosedResultSetMetaDataChecker(ResultSetMetaData.class, resultSetMetaDataOfClosedResultSet); checker.testAllMethods(); if (checker.hadAnyFailures()) { fail("Already-closed exception error(s): \n" + checker.getReport()); } } @Test public void testClosedResultSetMetaDataMethodsThrowRight2() { ThrowsClosedBulkChecker<ResultSetMetaData> checker = new ClosedResultSetMetaDataChecker(ResultSetMetaData.class, resultSetMetaDataOfClosedStmt); checker.testAllMethods(); if (checker.hadAnyFailures()) { fail("Already-closed exception error(s): \n" + checker.getReport()); } } private static class ClosedDatabaseMetaDataChecker extends ThrowsClosedBulkChecker<DatabaseMetaData> { private static final String DATABASEMETADATA_CLOSED_MESSAGE = "DatabaseMetaData's Connection is already closed."; ClosedDatabaseMetaDataChecker(Class<DatabaseMetaData> intf, DatabaseMetaData jdbcObject) { super(intf, jdbcObject, DATABASEMETADATA_CLOSED_MESSAGE); } @Override protected boolean isOkayNonthrowingMethod(Method method) { return super.isOkayNonthrowingMethod(method) || method.getName().equals("getDriverMajorVersion") || method.getName().equals("getDriverMinorVersion") || method.getName().equals("getConnection") // TODO: New Java 8 methods not implemented in Avatica. || method.getName().equals("getMaxLogicalLobSize") || method.getName().equals("supportsRefCursors"); } @Override protected boolean isOkaySpecialCaseException(Method method, Throwable cause) { final boolean result; if (super.isOkaySpecialCaseException(method, cause)) { result = true; } else if (RuntimeException.class == cause.getClass() && normalClosedExceptionText.equals(cause.getMessage()) && method.getName().equals("getResultSetHoldability")) { // Special good-enough case--we had to use RuntimeException for now. result = true; } else { result = false; } return result; } } // class ClosedDatabaseMetaDataChecker @Test public void testClosedDatabaseMetaDataMethodsThrowRight() { ThrowsClosedBulkChecker<DatabaseMetaData> checker = new ClosedDatabaseMetaDataChecker(DatabaseMetaData.class, databaseMetaDataOfClosedConn); checker.testAllMethods(); if (checker.hadAnyFailures()) { fail("Already-closed exception error(s): \n" + checker.getReport()); } } }