/* * Copyright 2012 - 2014 Maginatics, Inc. * * Licensed 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 com.maginatics.jdbclint; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.concurrent.atomic.AtomicReference; import com.maginatics.jdbclint.Configuration.Check; /** * StatementProxy proxies a Statement adding some checks. * * * whether Statement was closed * * whether Statement was closed more than once * * whether Statement addBatch was called without executeBatch */ final class StatementProxy implements InvocationHandler { private final Statement stmt; private final Configuration config; private final String className; private final Exception exception = new SQLException(); private enum State { OPENED, IN_ADD_BATCH, EXECUTED, CLOSED; } private final AtomicReference<State> state = new AtomicReference<State>(State.OPENED); private final boolean checkDoubleClose; private final boolean checkMissingClose; private final boolean checkMissingExecute; private final boolean checkMissingExecuteBatch; private final ConnectionProxy connectionProxy; static Statement newInstance(final ConnectionProxy connectionProxy, final Statement stmt, final Configuration config) { return (Statement) Proxy.newProxyInstance( stmt.getClass().getClassLoader(), new Class<?>[] {Statement.class}, new StatementProxy(connectionProxy, stmt, config)); } static PreparedStatement newInstance(final ConnectionProxy connectionProxy, final PreparedStatement stmt, final Configuration config) { return (PreparedStatement) Proxy.newProxyInstance( stmt.getClass().getClassLoader(), new Class<?>[] {PreparedStatement.class}, new StatementProxy(connectionProxy, stmt, config)); } StatementProxy(final ConnectionProxy connectionProxy, final Statement stmt, final Configuration config) { this.connectionProxy = Utils.checkNotNull(connectionProxy); this.stmt = Utils.checkNotNull(stmt); this.config = Utils.checkNotNull(config); this.className = "Statement"; checkDoubleClose = config.isEnabled(Check.STATEMENT_DOUBLE_CLOSE); checkMissingClose = config.isEnabled(Check.STATEMENT_MISSING_CLOSE); checkMissingExecute = config.isEnabled(Check.STATEMENT_MISSING_EXECUTE); checkMissingExecuteBatch = config.isEnabled( Check.STATEMENT_MISSING_EXECUTE_BATCH); } StatementProxy(final ConnectionProxy connectionProxy, final PreparedStatement stmt, final Configuration config) { this.connectionProxy = Utils.checkNotNull(connectionProxy); this.stmt = Utils.checkNotNull(stmt); this.config = Utils.checkNotNull(config); this.className = "PreparedStatement"; checkDoubleClose = config.isEnabled( Check.PREPARED_STATEMENT_DOUBLE_CLOSE); checkMissingClose = config.isEnabled( Check.PREPARED_STATEMENT_MISSING_CLOSE); checkMissingExecute = config.isEnabled( Check.PREPARED_STATEMENT_MISSING_EXECUTE); checkMissingExecuteBatch = config.isEnabled( Check.PREPARED_STATEMENT_MISSING_EXECUTE_BATCH); } @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { String name = method.getName(); if (name.equals("addBatch")) { state.set(State.IN_ADD_BATCH); } else if (name.equals("executeBatch") || name.equals("executeLargeBatch")) { state.set(State.EXECUTED); } else if (name.equals("execute") || name.equals("executeLargeUpdate") || name.equals("executeQuery") || name.equals("executeUpdate")) { state.set(State.EXECUTED); } else if (name.equals("close")) { if (checkDoubleClose && state.get() == State.CLOSED) { // Closing the same statement twice can cause issues with // server-side statements. Utils.fail(config, exception, className + " already closed"); } else if (checkMissingExecute && state.compareAndSet(State.OPENED, State.CLOSED)) { stmt.close(); Utils.fail(config, exception, className + " without execute"); } else if (checkMissingExecuteBatch && state.compareAndSet(State.IN_ADD_BATCH, State.CLOSED)) { stmt.close(); Utils.fail(config, exception, className + " addBatch without executeBatch"); } state.set(State.CLOSED); return null; } // Be conservative and mark connection as non-readonly for all execute // calls except executeQuery if (name.startsWith("execute") && !name.equals("executeQuery")) { connectionProxy.setReadOnly(false); } Object returnVal; try { returnVal = method.invoke(stmt, args); } catch (InvocationTargetException ite) { throw ite.getTargetException(); } if (name.equals("executeQuery") || name.equals("getGeneratedKeys") || name.equals("getResultSet")) { if (returnVal != null) { returnVal = ResultSetProxy.newInstance((ResultSet) returnVal, config); } } return returnVal; } @Override protected void finalize() throws SQLException { if (checkMissingClose && state.get() != State.CLOSED) { Utils.fail(config, exception, className + " not closed"); } } }