/*
* Copyright 2014-2015 the original author or authors
*
* 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.wplatform.ddal.shards;
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.SQLException;
/**
* @author <a href="mailto:jorgie.mail@gmail.com">jorgie li</a>
*/
public final class SmartConnection extends SmartSupport implements InvocationHandler {
private String username;
private String password;
private Boolean readOnly = Boolean.FALSE;
private Integer transactionIsolation;
private Boolean autoCommit;
private boolean closed = false;
private Connection target;
/**
* @param database
* @param dataSource
* @param traceable
* @throws SQLException
*/
protected SmartConnection(DataSourceRepository database, SmartDataSource dataSource) {
super(database, dataSource);
}
/**
* @param database
* @param dataSource
* @param username
* @param password
* @throws SQLException
*/
protected SmartConnection(DataSourceRepository database, SmartDataSource dataSource, String username,
String password) {
this(database, dataSource);
this.username = username;
this.password = password;
}
/**
* Creates a exception trace version of a connection
*
* @param conn - the original connection
* @return - the connection with exception trace
* @throws SQLException
*/
public static Connection newInstance(DataSourceRepository database, SmartDataSource dataSource) {
InvocationHandler handler = new SmartConnection(database, dataSource);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[] { Connection.class }, handler);
}
/**
* Creates a exception trace version of a connection
*
* @param conn - the original connection
* @return - the connection with exception trace
* @throws SQLException
*/
public static Connection newInstance(DataSourceRepository database, SmartDataSource dataSource, String username,
String password) {
InvocationHandler handler = new SmartConnection(database, dataSource, username, password);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[] { Connection.class }, handler);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Invocation on ConnectionProxy interface coming in...
if (method.getName().equals("equals")) {
// We must avoid fetching a target Connection for "equals".
// Only consider equal when proxies are identical.
return (proxy == args[0]);
} else if (method.getName().equals("hashCode")) {
// We must avoid fetching a target Connection for "hashCode",
// and we must return the same hash code even when the target
// Connection has been fetched: use hashCode of Connection proxy.
return System.identityHashCode(proxy);
} else if (method.getName().equals("unwrap")) {
if (((Class<?>) args[0]).isInstance(proxy)) {
return proxy;
}
} else if (method.getName().equals("isWrapperFor")) {
if (((Class<?>) args[0]).isInstance(proxy)) {
return true;
}
}
if (!hasTargetConnection()) {
// No physical target Connection kept yet ->
// resolve transaction demarcation methods without fetching
// a physical JDBC Connection until absolutely necessary.
if (method.getName().equals("toString")) {
return "Routing Connection proxy for RoutingDataSource [" + dataSource + "]";
} else if (method.getName().equals("isReadOnly")) {
return this.readOnly;
} else if (method.getName().equals("setReadOnly")) {
this.readOnly = (Boolean) args[0];
return null;
} else if (method.getName().equals("getTransactionIsolation")) {
if (this.transactionIsolation != null) {
return this.transactionIsolation;
}
// Else fetch actual Connection and check there,
// because we didn't have a default specified.
} else if (method.getName().equals("setTransactionIsolation")) {
this.transactionIsolation = (Integer) args[0];
return null;
} else if (method.getName().equals("getAutoCommit")) {
if (this.autoCommit != null) {
return this.autoCommit;
}
// Else fetch actual Connection and check there,
// because we didn't have a default specified.
} else if (method.getName().equals("setAutoCommit")) {
this.autoCommit = (Boolean) args[0];
return null;
} else if (method.getName().equals("commit")) {
// Ignore: no statements created yet.
return null;
} else if (method.getName().equals("rollback")) {
// Ignore: no statements created yet.
return null;
} else if (method.getName().equals("getWarnings")) {
return null;
} else if (method.getName().equals("clearWarnings")) {
return null;
} else if (method.getName().equals("close")) {
// Ignore: no target connection yet.
this.closed = true;
return null;
} else if (method.getName().equals("isClosed")) {
return this.closed;
} else if (this.closed) {
// Connection proxy closed, without ever having fetched a
// physical JDBC Connection: throw corresponding SQLException.
throw new SQLException("Illegal operation: connection is closed");
}
}
// Target Connection already fetched,
// or target Connection necessary for current operation ->
// invoke method on target connection.
try {
return method.invoke(getTargetConnection(method), args);
} catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
/**
* Return whether the proxy currently holds a target Connection.
*/
private boolean hasTargetConnection() {
return (this.target != null);
}
/**
* Return the target Connection, fetching it and initializing it if
* necessary.
*/
private Connection getTargetConnection(Method operation) throws SQLException {
if (this.target == null) {
// No target Connection held -> fetch one.
debug("Connecting to database for operation '" + operation.getName() + "'");
// Fetch physical Connection from DataSource.
this.target = (this.username != null) ? applyConnection(this.readOnly, this.username, this.password)
: applyConnection(this.readOnly);
// Apply kept transaction settings, if any.
if (this.readOnly) {
try {
this.target.setReadOnly(this.readOnly);
} catch (Exception ex) {
// "read-only not supported" -> ignore, it's just a hint
// anyway
if (trace.isDebugEnabled()) {
trace.debug(ex, "Could not set JDBC Connection read-only");
}
}
}
if (this.transactionIsolation != null) {
this.target.setTransactionIsolation(this.transactionIsolation);
}
if (this.autoCommit != null) {
this.target.setAutoCommit(this.autoCommit);
}
} else {
// Target Connection already held -> return it.
debug("Using existing database connection for operation '" + operation.getName() + "'");
}
return this.target;
}
}