CachingConnectionProvider.java
/*
*
* The DbUnit Database Testing Framework
* Copyright (C)2002-2026, DbUnit.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package org.dbunit.database;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.Callable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Caches a single {@link IDatabaseConnection} so that it - and the table
* metadata it accumulates - can be reused across many test methods instead of
* being rebuilt on every call.
*
* <p>
* A {@link Callable} supplies the connection-creation logic; the factory is
* only invoked when there is no cached connection yet, or when the
* previously cached one is no longer {@linkplain Connection#isValid(int)
* alive}. A dead connection is closed and transparently replaced rather than
* returned to the caller or left for the next call to fail on.
*
* <p>
* <b>Usage.</b> Construct one instance per target database and share that
* same instance across the {@link org.dbunit.JdbcDatabaseTester},
* {@link org.dbunit.DataSourceDatabaseTester}, {@link org.dbunit.JndiDatabaseTester}
* or {@link org.dbunit.DefaultDatabaseTester} instances that are created for
* each test, for example via a {@code static} field on a common test base
* class:
*
* <pre>
* private static final CachingConnectionProvider CONNECTION_PROVIDER =
* new CachingConnectionProvider();
*
* @BeforeEach
* void setUp() throws Exception
* {
* final IDatabaseTester tester = new JdbcDatabaseTester(driverClass,
* connectionUrl, username, password, schema, CONNECTION_PROVIDER);
* // The default IOperationListener closes the connection after every
* // onSetup()/onTearDown() call, which would defeat the cache. Pair it
* // with a listener that leaves the connection open, e.g.:
* tester.setOperationListener(IOperationListener.NO_OP_OPERATION_LISTENER);
* ...
* }
* </pre>
*
* <p>
* <b>Thread safety.</b> Access to the cached connection is synchronized, so
* concurrent callers cannot create or replace it at the same time. The
* returned {@link IDatabaseConnection} - and the underlying JDBC
* {@link Connection} it wraps - is however not synchronized, so this class is
* only appropriate for test suites that run sequentially, not for test
* methods executing concurrently against the same cached connection.
*
* <p>
* <b>Metadata staleness.</b> Because the whole point of reuse is to avoid
* re-fetching table metadata, a cached connection's {@code DatabaseDataSet}
* does not notice schema changes (new/dropped/altered tables) made after it
* was first cached. Do not share a provider across tests that alter DDL
* mid-run.
*
* @since 3.4.0
*/
public class CachingConnectionProvider
{
private static final Logger logger =
LoggerFactory.getLogger(CachingConnectionProvider.class);
/**
* Default number of seconds {@link #getConnection(Callable)} allows
* {@link Connection#isValid(int)} to take when checking whether the
* cached connection is still alive.
*/
public static final int DEFAULT_VALIDATION_TIMEOUT_SECONDS = 5;
private final int validationTimeoutSeconds;
private IDatabaseConnection connection;
/**
* Creates a provider that validates the cached connection with the
* {@link #DEFAULT_VALIDATION_TIMEOUT_SECONDS default validation timeout}.
*/
public CachingConnectionProvider()
{
this(DEFAULT_VALIDATION_TIMEOUT_SECONDS);
}
/**
* Creates a provider that validates the cached connection with the given
* timeout.
*
* @param validationTimeoutSeconds
* The number of seconds {@link Connection#isValid(int)} is
* allowed to take when checking whether the cached connection
* is still alive. Zero means no timeout is applied.
*/
public CachingConnectionProvider(final int validationTimeoutSeconds)
{
if (validationTimeoutSeconds < 0)
{
throw new IllegalArgumentException("The parameter "
+ "'validationTimeoutSeconds' must not be negative");
}
this.validationTimeoutSeconds = validationTimeoutSeconds;
}
/**
* Returns the cached connection, creating it with the given factory on
* the first call and again whenever the previously cached connection is
* no longer alive.
*
* @param connectionFactory
* Creates a new connection. Only invoked when there is no live
* cached connection to reuse.
* @return The cached, live connection.
* @throws Exception
* If {@code connectionFactory} throws while creating a new
* connection.
*/
public synchronized IDatabaseConnection getConnection(
final Callable<IDatabaseConnection> connectionFactory) throws Exception
{
if (connection != null && isAlive(connection))
{
logger.debug("getConnection() - reusing cached connection {}", connection);
return connection;
}
if (connection != null)
{
logger.debug("getConnection() - cached connection {} is no longer"
+ " alive, replacing it", connection);
closeQuietly(connection);
connection = null;
}
connection = connectionFactory.call();
return connection;
}
/**
* Closes and discards the cached connection, if any. The next call to
* {@link #getConnection(Callable)} creates a fresh one.
*
* @throws SQLException
* If closing the cached connection fails.
*/
public synchronized void close() throws SQLException
{
if (connection == null)
{
return;
}
try
{
connection.close();
}
finally
{
connection = null;
}
}
private boolean isAlive(final IDatabaseConnection candidate)
{
try
{
final Connection jdbcConnection = candidate.getConnection();
return !jdbcConnection.isClosed()
&& jdbcConnection.isValid(validationTimeoutSeconds);
} catch (final SQLException e)
{
logger.debug("isAlive() - liveness check failed for connection {}",
candidate, e);
return false;
}
}
private void closeQuietly(final IDatabaseConnection candidate)
{
try
{
candidate.close();
} catch (final SQLException e)
{
logger.warn("closeQuietly() - exception while closing the stale"
+ " cached connection", e);
}
}
@Override
public synchronized String toString()
{
final StringBuilder sb = new StringBuilder();
sb.append(getClass().getName()).append("[");
sb.append("validationTimeoutSeconds=").append(validationTimeoutSeconds);
sb.append(", connection=").append(connection);
sb.append("]");
return sb.toString();
}
}