/*
* Copyright 2002-2005 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 org.springframework.jdbc.datasource;
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.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
*
* @author Thomas Risberg
*/
public class SingleUseDataSource extends SingleConnectionDataSource {
private static final int DEFAULT_CACHE_ENTRIES = 10;
private int maxStatements = DEFAULT_CACHE_ENTRIES;
/** Creates a new instance of SingleUseDataSource */
public SingleUseDataSource() {
}
public int getMaxStatements() {
return maxStatements;
}
public void setMaxStatements(int maxStatements) {
this.maxStatements = maxStatements;
}
/**
* Override getting a connection using the static from DriverManager providing
* a connection proxy that allows caching of prepared statements.
* @see java.sql.DriverManager#getConnection(String, java.util.Properties)
*/
protected Connection getConnectionFromDriverManager(String url, Properties props)
throws SQLException {
if(this.maxStatements > 0) {
if (logger.isDebugEnabled()) {
logger.debug("Creating new JDBC Connection to [" + url + "] with a statement cache of " + maxStatements);
}
return getStatementCachingConnectionProxy(DriverManager.getConnection(url, props));
}
else {
return super.getConnectionFromDriverManager(url, props);
}
}
/**
* Wrap the given Connection with a proxy that delegates every method call to it
* and caches any prepared statements.
* @param target the original Connection to wrap
* @return the wrapped Connection
*/
protected Connection getStatementCachingConnectionProxy(Connection target) {
return (Connection) Proxy.newProxyInstance(
ConnectionProxy.class.getClassLoader(),
new Class[] {ConnectionProxy.class},
new StatementCachingInvocationHandler(target, maxStatements));
}
/**
* Invocation handler that suppresses close calls on JDBC Connections.
*/
private static class StatementCachingInvocationHandler implements InvocationHandler {
protected final Log logger = LogFactory.getLog(getClass());
private static final String GET_TARGET_CONNECTION_METHOD_NAME = "getTargetConnection";
private static final String PREPARE_STATEMENT_METHOD_NAME = "prepareStatement";
private static final String CONNECTION_CLOSE_METHOD_NAME = "close";
private final Connection target;
private Map statementCache;
private int cacheSize;
public StatementCachingInvocationHandler(Connection target, int maxStatements) {
this.target = target;
this.cacheSize = maxStatements;
statementCache = new LinkedHashMap(cacheSize, .75F, true) {
public boolean removeEldestEntry(Map.Entry eldest) {
if (size() > cacheSize) {
close(eldest.getKey());
return true;
}
else {
return false;
}
}
public void close(Object key) {
try {
((PreparedStatementProxy)get(key)).getTargetPreparedStatement().close();
}
catch (SQLException se) {
logger.warn("Exception during statement cache cleanup: [" + se.getClass().getName() + "] " + se.getMessage());
}
}
public void clear() {
Object[] keys = statementCache.keySet().toArray();
for (int i = 0; i < keys.length; i++) {
close(keys[i]);
}
super.clear();
}
public Object remove(Object key) {
close(key);
return super.remove(key);
}
};
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Handle getTargetConnection method: return underlying connection.
if (method.getName().equals(GET_TARGET_CONNECTION_METHOD_NAME)) {
return this.target;
}
// Handle close method: close cache and then pass the call on.
if (method.getName().equals(CONNECTION_CLOSE_METHOD_NAME)) {
statementCache.clear();
// Invoke close method on target connection.
try {
return method.invoke(this.target, args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
// Handle prepareStatement method
if (method.getName().equals(PREPARE_STATEMENT_METHOD_NAME)) {
// Invoke method on target connection.
PreparedStatement ps = (PreparedStatement)statementCache.get(args[0]);
if (ps == null) {
try {
PreparedStatement newPs = (PreparedStatement)method.invoke(this.target, args);
ps = getCloseSuppressingPreparedStatementProxy(newPs);
statementCache.put(args[0], ps);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
else {
try {
ps.clearParameters();
}
catch (SQLException ignore) {}
try {
ps.clearBatch();
}
catch (SQLException ignore) {}
try {
ps.clearWarnings();
}
catch (SQLException ignore) {}
}
return ps;
}
// Invoke method on target connection.
try {
return method.invoke(this.target, args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
/**
* Wrap the given PreparedStatement with a proxy that delegates every method call to it
* but suppresses close calls.
* @param target the original PreparedStatement to wrap
* @return the wrapped PreparedStatement
*/
protected PreparedStatement getCloseSuppressingPreparedStatementProxy(PreparedStatement target) {
return (PreparedStatement) Proxy.newProxyInstance(
PreparedStatementProxy.class.getClassLoader(),
new Class[] {PreparedStatementProxy.class},
new CloseSuppressingInvocationHandler(target));
}
/**
* Invocation handler that suppresses close calls on JDBC Statements.
*/
private static class CloseSuppressingInvocationHandler implements InvocationHandler {
private static final String GET_TARGET_PREPARED_STATEMENT_METHOD_NAME = "getTargetPreparedStatement";
private static final String PREPARED_STATEMENT_CLOSE_METHOD_NAME = "close";
private final PreparedStatement target;
public CloseSuppressingInvocationHandler(PreparedStatement target) {
this.target = target;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Invocation on ConnectionProxy interface coming in...
// Handle getTargetConnection method: return underlying connection.
if (method.getName().equals(GET_TARGET_PREPARED_STATEMENT_METHOD_NAME)) {
return this.target;
}
// Handle close method: don't pass the call on.
if (method.getName().equals(PREPARED_STATEMENT_CLOSE_METHOD_NAME)) {
return null;
}
// Invoke method on target connection.
try {
return method.invoke(this.target, args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}
}
}