/*
* 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.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import com.maginatics.jdbclint.Configuration.Check;
/**
* ConnectionProxy proxies a Connection adding some checks.
*
* * whether Connection was closed
* * whether Connection was committed or rolled back
*/
public final class ConnectionProxy implements InvocationHandler {
private final Connection conn;
private final Configuration config;
private final Exception exception = new SQLException();
private final AtomicBoolean readOnly = new AtomicBoolean(true);
private enum State {
OPENED,
IN_TRANSACTION,
COMMITTED,
CLOSED;
}
private final AtomicReference<State> state =
new AtomicReference<State>(State.OPENED);
/**
* Create a ConnectionProxy.
*
* @param conn Connection to proxy
* @param config configuration
* @return proxied Connection
*/
public static Connection newInstance(final Connection conn,
final Configuration config) {
return (Connection) Proxy.newProxyInstance(
conn.getClass().getClassLoader(),
new Class<?>[] {Connection.class},
new ConnectionProxy(conn, config));
}
ConnectionProxy(final Connection conn, final Configuration config) {
this.conn = Utils.checkNotNull(conn);
this.config = Utils.checkNotNull(config);
}
@Override
public Object invoke(final Object proxy, final Method method,
final Object[] args) throws Throwable {
String name = method.getName();
if (name.equals("close")) {
if (config.isEnabled(Check.CONNECTION_DOUBLE_CLOSE) &&
state.get() == State.CLOSED) {
Utils.fail(config, exception, "Connection already closed");
} else if (config.isEnabled(
Check.CONNECTION_MISSING_COMMIT_OR_ROLLBACK) &&
!conn.getAutoCommit() &&
state.compareAndSet(State.IN_TRANSACTION, State.CLOSED)) {
conn.close();
Utils.fail(config, exception,
"Connection did not commit or roll back");
} else if (config.isEnabled(
Check.CONNECTION_MISSING_PREPARE_STATEMENT) &&
state.compareAndSet(State.OPENED, State.CLOSED)) {
conn.close();
Utils.fail(config, exception,
"Connection without prepareStatement");
}
state.set(State.CLOSED);
if (config.isEnabled(Check.CONNECTION_MISSING_READ_ONLY) &&
isReadOnly() && !conn.isReadOnly()) {
conn.close();
Utils.fail(config, exception,
"Connection did not execute updates, " +
"consider calling setReadOnly");
}
return null;
}
if (name.equals("commit") || name.equals("rollback")) {
state.set(State.COMMITTED);
}
Object returnVal;
try {
returnVal = method.invoke(conn, args);
} catch (InvocationTargetException ite) {
throw ite.getTargetException();
}
if (name.equals("createStatement")) {
state.set(State.IN_TRANSACTION);
returnVal = StatementProxy.newInstance(this,
(Statement) returnVal, config);
} else if (name.equals("prepareStatement")) {
state.set(State.IN_TRANSACTION);
returnVal = StatementProxy.newInstance(this,
(PreparedStatement) returnVal, config);
}
return returnVal;
}
@Override
protected void finalize() throws SQLException {
if (config.isEnabled(Check.CONNECTION_MISSING_CLOSE) &&
state.get() != State.CLOSED) {
Utils.fail(config, exception, "Connection not closed");
}
}
public void setReadOnly(final boolean readOnly) {
this.readOnly.set(readOnly);
}
public boolean isReadOnly() {
return readOnly.get();
}
}