diff --git a/gradle.properties b/gradle.properties index 111f147..773fe64 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ group = org.xbib name = database -version = 2.2.0 +version = 2.3.0 diff --git a/jdbc-oracle/src/main/java/module-info.java b/jdbc-oracle/src/main/java/module-info.java index 16a596e..d322611 100644 --- a/jdbc-oracle/src/main/java/module-info.java +++ b/jdbc-oracle/src/main/java/module-info.java @@ -3,7 +3,6 @@ import org.xbib.jdbc.query.Flavor; module org.xbib.jdbc.oracle { requires org.xbib.jdbc.query; - requires org.xbib.jdbc.connection.pool; requires com.oracle.database.jdbc; requires java.sql; uses Flavor; diff --git a/jdbc-oracle/src/main/java/org/xbib/jdbc/oracle/Oracle.java b/jdbc-oracle/src/main/java/org/xbib/jdbc/oracle/Oracle.java index 8a88d2d..8210322 100644 --- a/jdbc-oracle/src/main/java/org/xbib/jdbc/oracle/Oracle.java +++ b/jdbc-oracle/src/main/java/org/xbib/jdbc/oracle/Oracle.java @@ -1,7 +1,6 @@ package org.xbib.jdbc.oracle; import oracle.jdbc.OraclePreparedStatement; -import org.xbib.jdbc.connection.pool.ProxyPreparedStatement; import org.xbib.jdbc.query.Flavor; import java.sql.PreparedStatement; @@ -45,7 +44,13 @@ public class Oracle implements Flavor { return "binary_float"; } - @Override + public void setFloat(PreparedStatement preparedStatement, int i, Float floatValue) throws SQLException { + if (preparedStatement instanceof OraclePreparedStatement) { + ((OraclePreparedStatement) preparedStatement).setBinaryFloat(i, floatValue); + } + } + + /*@Override public void setFloat(PreparedStatement preparedStatement, int i, Float floatValue) throws SQLException { if (preparedStatement instanceof ProxyPreparedStatement) { ProxyPreparedStatement proxyPreparedStatement = (ProxyPreparedStatement) preparedStatement; @@ -53,14 +58,20 @@ public class Oracle implements Flavor { } else { ((OraclePreparedStatement) preparedStatement).setBinaryFloat(i, floatValue); } - } + }*/ @Override public String typeDouble() { return "binary_double"; } - @Override + public void setDouble(PreparedStatement preparedStatement, int i, Double doubleValue) throws SQLException { + if (preparedStatement instanceof OraclePreparedStatement) { + ((OraclePreparedStatement) preparedStatement).setBinaryDouble(i, doubleValue); + } + } + + /*@Override public void setDouble(PreparedStatement preparedStatement, int i, Double doubleValue) throws SQLException { if (preparedStatement instanceof ProxyPreparedStatement) { ProxyPreparedStatement proxyPreparedStatement = (ProxyPreparedStatement) preparedStatement; @@ -68,7 +79,7 @@ public class Oracle implements Flavor { } else { ((OraclePreparedStatement) preparedStatement).setBinaryDouble(i, doubleValue); } - } + }*/ @Override public String typeBigDecimal(int size, int precision) { diff --git a/jdbc-pool/src/main/java/module-info.java b/jdbc-pool/src/main/java/module-info.java new file mode 100644 index 0000000..8fc00b1 --- /dev/null +++ b/jdbc-pool/src/main/java/module-info.java @@ -0,0 +1,13 @@ +module org.xbib.jdbc.pool { + uses org.xbib.jdbc.pool.api.XbibDataSourceProvider; + requires java.security.jgss; + requires java.sql; + requires java.transaction.xa; + exports org.xbib.jdbc.pool.api; + exports org.xbib.jdbc.pool.api.cache; + exports org.xbib.jdbc.pool.api.configuration; + exports org.xbib.jdbc.pool.api.exceptionsorter; + exports org.xbib.jdbc.pool.api.security; + exports org.xbib.jdbc.pool.api.transaction; + exports org.xbib.jdbc.pool; +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/ConnectionFactory.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/ConnectionFactory.java new file mode 100644 index 0000000..6adfa61 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/ConnectionFactory.java @@ -0,0 +1,312 @@ +package org.xbib.jdbc.pool; + +import java.lang.reflect.InvocationTargetException; +import java.security.Principal; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Locale; +import java.util.Properties; +import javax.sql.XAConnection; +import org.xbib.jdbc.pool.api.XbibDataSourceListener; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionFactoryConfiguration; +import org.xbib.jdbc.pool.api.security.XbibSecurityProvider; +import org.xbib.jdbc.pool.api.transaction.TransactionIntegration.ResourceRecoveryFactory; +import org.xbib.jdbc.pool.util.PropertyInjector; +import org.xbib.jdbc.pool.util.XAConnectionAdaptor; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionFactoryConfiguration.TransactionIsolation.UNDEFINED; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnWarning; + +public final class ConnectionFactory implements ResourceRecoveryFactory { + + private static final String URL_PROPERTY_NAME = "url"; + + private static final Properties EMPTY_PROPERTIES = new Properties(); + + private final XbibConnectionFactoryConfiguration configuration; + private final XbibDataSourceListener[] listeners; + private final Properties jdbcProperties = new Properties(); // backup of jdbcProperties for DRIVER mode + private final Mode factoryMode; + + // these are the sources for connections, that will be used depending on the mode + private java.sql.Driver driver; + private javax.sql.DataSource dataSource; + private javax.sql.XADataSource xaDataSource; + private javax.sql.XADataSource xaRecoveryDataSource; + + private PropertyInjector injector; + private Integer defaultIsolationLevel; + + public ConnectionFactory(XbibConnectionFactoryConfiguration configuration, XbibDataSourceListener... listeners) { + this.configuration = configuration; + this.listeners = listeners; + + factoryMode = Mode.fromClass(configuration.connectionProviderClass()); + switch (factoryMode) { + case XA_DATASOURCE: + injector = new PropertyInjector(configuration.connectionProviderClass()); + Properties xaProperties = configuration.xaProperties().isEmpty() ? configuration.jdbcProperties() : configuration.xaProperties(); + xaDataSource = newXADataSource(xaProperties); + xaRecoveryDataSource = newXADataSource(xaProperties); + break; + case DATASOURCE: + injector = new PropertyInjector(configuration.connectionProviderClass()); + dataSource = newDataSource(configuration.jdbcProperties()); + break; + case DRIVER: + driver = newDriver(); + jdbcProperties.putAll(configuration.jdbcProperties()); + break; + } + } + + public int defaultJdbcIsolationLevel() { + return defaultIsolationLevel == null ? UNDEFINED.level() : defaultIsolationLevel; + } + + @SuppressWarnings("StringConcatenation") + private javax.sql.XADataSource newXADataSource(Properties properties) { + javax.sql.XADataSource newDataSource; + try { + newDataSource = configuration.connectionProviderClass().asSubclass(javax.sql.XADataSource.class).getDeclaredConstructor().newInstance(); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException("Unable to instantiate javax.sql.XADataSource", e); + } + + if (configuration.jdbcUrl() != null && !configuration.jdbcUrl().isEmpty()) { + injectUrlProperty(newDataSource, URL_PROPERTY_NAME, configuration.jdbcUrl()); + } + try { + newDataSource.setLoginTimeout((int) configuration.loginTimeout().getSeconds()); + } catch (SQLException e) { + fireOnWarning(listeners, "Unable to set login timeout: " + e.getMessage()); + } + + injectJdbcProperties(newDataSource, properties); + return newDataSource; + } + + @SuppressWarnings("StringConcatenation") + private javax.sql.DataSource newDataSource(Properties properties) { + javax.sql.DataSource newDataSource; + try { + newDataSource = configuration.connectionProviderClass().asSubclass(javax.sql.DataSource.class).getDeclaredConstructor().newInstance(); + } catch (IllegalAccessException | InstantiationException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException("Unable to instantiate javax.sql.DataSource", e); + } + + if (configuration.jdbcUrl() != null && !configuration.jdbcUrl().isEmpty()) { + injectUrlProperty(newDataSource, URL_PROPERTY_NAME, configuration.jdbcUrl()); + } + try { + newDataSource.setLoginTimeout((int) configuration.loginTimeout().getSeconds()); + } catch (SQLException e) { + fireOnWarning(listeners, "Unable to set login timeout: " + e.getMessage()); + } + + injectJdbcProperties(newDataSource, properties); + return newDataSource; + } + + @SuppressWarnings("StringConcatenation") + private java.sql.Driver newDriver() { + DriverManager.setLoginTimeout((int) configuration.loginTimeout().getSeconds()); + + if (configuration.connectionProviderClass() == null) { + try { + return driver = DriverManager.getDriver(configuration.jdbcUrl()); + } catch (SQLException sql) { + throw new RuntimeException("Unable to get java.sql.Driver from DriverManager", sql); + } + } else { + try { + driver = configuration.connectionProviderClass().asSubclass(java.sql.Driver.class).getDeclaredConstructor().newInstance(); + if (!driver.acceptsURL(configuration.jdbcUrl())) { + fireOnWarning(listeners, "Driver does not support the provided URL: " + configuration.jdbcUrl()); + } + return driver; + } catch (IllegalAccessException | InstantiationException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException("Unable to instantiate java.sql.Driver", e); + } catch (SQLException e) { + throw new RuntimeException("Unable to verify that the java.sql.Driver supports the provided URL", e); + } + } + } + + @SuppressWarnings("StringConcatenation") + private void injectUrlProperty(Object target, String propertyName, String propertyValue) { + try { + injector.inject(target, propertyName, propertyValue); + } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + // AG-134 - Some drivers have setURL() instead of setUrl(), so we retry with upper case. + // AG-228 - Not all (XA)DataSource's require an url to be set + if (propertyName.chars().anyMatch(Character::isLowerCase)) { + injectUrlProperty(target, propertyName.toUpperCase(Locale.ROOT), propertyValue); + } + } + } + + @SuppressWarnings({"ObjectAllocationInLoop", "StringConcatenation"}) + private void injectJdbcProperties(Object target, Properties properties) { + boolean ignoring = false; + for (String propertyName : properties.stringPropertyNames()) { + try { + injector.inject(target, propertyName, properties.getProperty(propertyName)); + } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + fireOnWarning(listeners, "Ignoring property '" + propertyName + "': " + e.getMessage()); + ignoring = true; + } + } + if (ignoring) { + fireOnWarning(listeners, "Available properties " + Arrays.toString(injector.availableProperties().toArray())); + } + } + + // --- // + + private Properties jdbcProperties() { + Properties properties = new Properties(); + properties.putAll(jdbcProperties); + properties.putAll(securityProperties(configuration.principal(), configuration.credentials())); + return properties; + } + + private Properties recoveryProperties() { + Properties properties = new Properties(); + if (hasRecoveryCredentials()) { + properties.putAll(securityProperties(configuration.recoveryPrincipal(), configuration.recoveryCredentials())); + } else { + // use the main credentials when recovery credentials are not provided + properties.putAll(securityProperties(configuration.principal(), configuration.credentials())); + } + return properties; + } + + private Properties securityProperties(Principal principal, Iterable credentials) { + Properties properties = new Properties(); + properties.putAll(securityProperties(principal)); + for (Object credential : credentials) { + properties.putAll(securityProperties(credential)); + } + return properties; + } + + private Properties securityProperties(Object securityObject) { + if (securityObject == null) { + return EMPTY_PROPERTIES; + } + + for (XbibSecurityProvider provider : configuration.securityProviders()) { + Properties properties = provider.getSecurityProperties(securityObject); + if (properties != null) { + return properties; + } + } + fireOnWarning(listeners, "Unknown security object of type: " + securityObject.getClass().getName()); + return EMPTY_PROPERTIES; + } + + // --- // + + public XAConnection createConnection() throws SQLException { + switch (factoryMode) { + case DRIVER: + return new XAConnectionAdaptor(connectionSetup(driver.connect(configuration.jdbcUrl(), jdbcProperties()))); + case DATASOURCE: + injectJdbcProperties(dataSource, securityProperties(configuration.principal(), configuration.credentials())); + return new XAConnectionAdaptor(connectionSetup(dataSource.getConnection())); + case XA_DATASOURCE: + injectJdbcProperties(xaDataSource, securityProperties(configuration.principal(), configuration.credentials())); + return xaConnectionSetup(xaDataSource.getXAConnection()); + default: + throw new SQLException("Unknown connection factory mode"); + } + } + + @SuppressWarnings("MagicConstant") + private Connection connectionSetup(Connection connection) throws SQLException { + if (connection == null) { + // AG-90: Driver can return null if the URL is not supported (see java.sql.Driver#connect() documentation) + throw new SQLException("Driver does not support the provided URL: " + configuration.jdbcUrl()); + } + + connection.setAutoCommit(configuration.autoCommit()); + if (configuration.jdbcTransactionIsolation().isDefined()) { + connection.setTransactionIsolation(configuration.jdbcTransactionIsolation().level()); + } else if (defaultIsolationLevel == null) { + defaultIsolationLevel = connection.getTransactionIsolation(); + } + if (configuration.initialSql() != null && !configuration.initialSql().isEmpty()) { + try (Statement statement = connection.createStatement()) { + statement.execute(configuration.initialSql()); + } + } + return connection; + } + + private XAConnection xaConnectionSetup(XAConnection xaConnection) throws SQLException { + if (xaConnection.getXAResource() == null) { + // Make sure that XAConnections are not processed as non-XA connections by the pool + xaConnection.close(); + throw new SQLException("null XAResource from XADataSource"); + } + try (Connection connection = xaConnection.getConnection()) { + connectionSetup(connection); + } + return xaConnection; + } + + // --- // + + public boolean hasRecoveryCredentials() { + return configuration.recoveryPrincipal() != null || (configuration.recoveryCredentials() != null && !configuration.recoveryCredentials().isEmpty()); + } + + @Override + public boolean isRecoverable() { + if (factoryMode == Mode.XA_DATASOURCE) { + return true; + } + fireOnWarning(listeners, "Recovery connections are only available for XADataSource"); + return false; + } + + @Override + public XAConnection getRecoveryConnection() throws SQLException { + if (isRecoverable()) { + injectJdbcProperties(xaRecoveryDataSource, recoveryProperties()); + return xaRecoveryDataSource.getXAConnection(); + } + // Fallback for wrong implemented TransactionIntegration + throw new SQLException("Recovery connections are only available for XADataSource"); + } + + // --- // + + private enum Mode { + + DRIVER, DATASOURCE, XA_DATASOURCE; + + @SuppressWarnings("WeakerAccess") + static Mode fromClass(Class providerClass) { + if (providerClass == null) { + return DRIVER; + } else if (javax.sql.XADataSource.class.isAssignableFrom(providerClass)) { + return XA_DATASOURCE; + } else if (javax.sql.DataSource.class.isAssignableFrom(providerClass)) { + return DATASOURCE; + } else if (java.sql.Driver.class.isAssignableFrom(providerClass)) { + return DRIVER; + } else { + throw new IllegalArgumentException("Unable to create ConnectionFactory from providerClass " + providerClass.getName()); + } + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/ConnectionHandler.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/ConnectionHandler.java new file mode 100644 index 0000000..52b03eb --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/ConnectionHandler.java @@ -0,0 +1,401 @@ +package org.xbib.jdbc.pool; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.time.Duration; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import javax.sql.XAConnection; +import javax.transaction.xa.XAResource; +import org.xbib.jdbc.pool.api.cache.Acquirable; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionFactoryConfiguration; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration; +import org.xbib.jdbc.pool.api.transaction.TransactionAware; +import org.xbib.jdbc.pool.util.AutoCloseableElement; +import org.xbib.jdbc.pool.util.UncheckedArrayList; +import org.xbib.jdbc.pool.wrapper.ConnectionWrapper; +import org.xbib.jdbc.pool.wrapper.XAConnectionWrapper; +import static java.lang.System.nanoTime; +import static java.lang.Thread.currentThread; +import static java.util.Arrays.copyOfRange; +import static java.util.EnumSet.noneOf; +import static java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater; +import static org.xbib.jdbc.pool.ConnectionHandler.DirtyAttribute.AUTOCOMMIT; +import static org.xbib.jdbc.pool.ConnectionHandler.DirtyAttribute.TRANSACTION_ISOLATION; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnWarning; + +public final class ConnectionHandler implements TransactionAware, Acquirable { + + private static final AtomicReferenceFieldUpdater stateUpdater = newUpdater(ConnectionHandler.class, State.class, "state"); + + private static final SQLCallable NO_ACTIVE_TRANSACTION = () -> false; + + private final XAConnection xaConnection; + + // Single Connection reference from xaConnection.getConnection() + private final Connection connection; + + // Single XAResource reference from xaConnection.getXAResource(). Can be null for no XA datasources. + private final XAResource xaResource; + + private final Pool connectionPool; + + // attributes that need to be reset when the connection is returned + private final Set dirtyAttributes = noneOf(DirtyAttribute.class); + + // collection of wrappers created while enlisted in the current transaction + private final AutoCloseableElement enlistedOpenWrappers = AutoCloseableElement.newHead(); + + // Can use annotation to get (in theory) a little better performance + // @Contended + private volatile State state = State.NEW; + + // for leak detection (only valid for CHECKED_OUT connections) + private Thread holdingThread; + + // Enhanced leak report + @SuppressWarnings("VolatileArrayField") + private volatile StackTraceElement[] acquisitionStackTrace; + private StackTraceElement[] lastOperationStackTrace; + private List connectionOperations; + + // for expiration (CHECKED_IN connections) and leak detection (CHECKED_OUT connections) + private long lastAccess; + + // flag to indicate that this the connection is enlisted to a transaction + private boolean enlisted; + + // reference to the task that flushes this connection when it gets over it's maxLifetime + private Future maxLifetimeTask; + + // Callback set by the transaction integration layer to prevent deferred enlistment + // If the connection is not associated with a transaction and an operation occurs within the bounds of a transaction, an SQLException is thrown + // If there is no transaction integration this should just return false + private SQLCallable transactionActiveCheck = NO_ACTIVE_TRANSACTION; + + public ConnectionHandler(XAConnection xa, Pool pool) throws SQLException { + xaConnection = xa; + connection = xaConnection.getConnection(); + xaResource = xaConnection.getXAResource(); + + connectionPool = pool; + touch(); + } + + public XAConnectionWrapper xaConnectionWrapper() { + return new XAConnectionWrapper(this, xaConnection, connectionPool.getConfiguration().connectionFactoryConfiguration().trackJdbcResources()); + } + + public ConnectionWrapper connectionWrapper() { + return new ConnectionWrapper(this, connectionPool.getConfiguration().connectionFactoryConfiguration().trackJdbcResources(), enlisted ? enlistedOpenWrappers : null); + } + + public ConnectionWrapper detachedWrapper() { + return new ConnectionWrapper(this, connectionPool.getConfiguration().connectionFactoryConfiguration().trackJdbcResources(), true); + } + + @SuppressWarnings("StringConcatenation") + public void onConnectionWrapperClose(ConnectionWrapper wrapper, ConnectionWrapper.JdbcResourcesLeakReport leakReport) throws SQLException { + if (leakReport.hasLeak()) { + fireOnWarning(connectionPool.getListeners(), "JDBC resources leaked: " + leakReport.resultSetCount() + " ResultSet(s) and " + leakReport.statementCount() + " Statement(s)"); + } + if (!enlisted && !wrapper.isDetached()) { + transactionEnd(); + } + } + + public Connection rawConnection() { + return connection; + } + + public XAResource getXaResource() { + return xaResource; + } + + @SuppressWarnings("MagicConstant") + public void resetConnection() throws SQLException { + transactionActiveCheck = NO_ACTIVE_TRANSACTION; + + if (!dirtyAttributes.isEmpty()) { + XbibConnectionFactoryConfiguration connectionFactoryConfiguration = connectionPool.getConfiguration().connectionFactoryConfiguration(); + + try { + if (dirtyAttributes.contains(AUTOCOMMIT)) { + connection.setAutoCommit(connectionFactoryConfiguration.autoCommit()); + } + if (dirtyAttributes.contains(TRANSACTION_ISOLATION)) { + XbibConnectionFactoryConfiguration.IsolationLevel isolation = connectionFactoryConfiguration.jdbcTransactionIsolation(); + connection.setTransactionIsolation(isolation.isDefined() ? isolation.level() : connectionPool.defaultJdbcIsolationLevel()); + } + // other attributes do not have default values in connectionFactoryConfiguration + } catch (SQLException se) { + setFlushOnly(se); + throw se; + } finally { + dirtyAttributes.clear(); + } + } + + try { + SQLWarning warning = connection.getWarnings(); + if (warning != null) { + XbibConnectionPoolConfiguration.ExceptionSorter exceptionSorter = connectionPool.getConfiguration().exceptionSorter(); + while (warning != null) { + if (exceptionSorter != null && exceptionSorter.isFatal(warning)) { + setState(State.FLUSH); + } + warning = warning.getNextWarning(); + } + connection.clearWarnings(); + } + } catch (SQLException se) { + setFlushOnly(se); + throw se; + // keep errors + } + } + + public void closeConnection() throws SQLException { + if (maxLifetimeTask != null && !maxLifetimeTask.isDone()) { + maxLifetimeTask.cancel(false); + } + maxLifetimeTask = null; + try { + State observedState = stateUpdater.get(this); + if (observedState != State.FLUSH) { + throw new SQLException("Closing connection in incorrect state " + observedState); + } + } finally { + try { + xaConnection.close(); + } finally { + stateUpdater.set(this, State.DESTROYED); + } + } + } + + public boolean acquire() { + return setState(State.CHECKED_IN, State.CHECKED_OUT); + } + + public boolean isAcquirable() { + State observedState = stateUpdater.get(this); + return observedState != State.FLUSH && observedState != State.DESTROYED; + } + + public boolean setState(State expected, State newState) { + if (expected == State.DESTROYED) { + throw new IllegalArgumentException("Trying to move out of state DESTROYED"); + } + + switch (newState) { + case NEW: + throw new IllegalArgumentException("Trying to set invalid state NEW"); + case CHECKED_IN: + case CHECKED_OUT: + case VALIDATION: + case FLUSH: + case DESTROYED: + return stateUpdater.compareAndSet(this, expected, newState); + default: + throw new IllegalArgumentException("Trying to set invalid state " + newState); + } + } + + public void setState(State newState) { + // Maybe could use lazySet here, but there doesn't seem to be any performance advantage + stateUpdater.set(this, newState); + } + + private boolean isActive() { + return stateUpdater.get(this) == State.CHECKED_OUT; + } + + public void touch() { + lastAccess = nanoTime(); + } + + public boolean isLeak(Duration timeout) { + return isActive() && !enlisted && isIdle(timeout); + } + + public boolean isIdle(Duration timeout) { + return nanoTime() - lastAccess > timeout.toNanos(); + } + + public void setMaxLifetimeTask(Future maxLifetimeTask) { + this.maxLifetimeTask = maxLifetimeTask; + } + + public boolean isValid() { + return connectionPool.getConfiguration().connectionValidator().isValid(detachedWrapper()); + } + + // --- Leak detection // + + public Thread getHoldingThread() { + return holdingThread; + } + + public void setHoldingThread(Thread holdingThread) { + this.holdingThread = holdingThread; + } + + // --- Enhanced leak report // + + /** + * Abbreviated list of all operation on the connection, for enhanced leak report + */ + @SuppressWarnings("VariableNotUsedInsideIf") + public void traceConnectionOperation(String operation) { + if (acquisitionStackTrace != null) { + connectionOperations.add(operation); + lastOperationStackTrace = currentThread().getStackTrace(); + } + } + + /** + * Abbreviated list of all operation on the connection, for enhanced leak report + */ + public List getConnectionOperations() { + return connectionOperations; + } + + /** + * Stack trace of the first acquisition for this connection + */ + public StackTraceElement[] getAcquisitionStackTrace() { + return acquisitionStackTrace == null ? null : copyOfRange(acquisitionStackTrace, 4, acquisitionStackTrace.length); + } + + /** + * Stores a stack trace for leak report. Setting a value != null also enables tracing of operations on the connection + */ + public void setAcquisitionStackTrace(StackTraceElement[] stackTrace) { + lastOperationStackTrace = null; + if (connectionOperations == null) { + connectionOperations = new UncheckedArrayList<>(String.class); + } + connectionOperations.clear(); + acquisitionStackTrace = stackTrace; + } + + /** + * Stack trace for the last operation on this connection + */ + public StackTraceElement[] getLastOperationStackTrace() { + return lastOperationStackTrace == null ? null : copyOfRange(lastOperationStackTrace, 3, lastOperationStackTrace.length); + } + + public void setDirtyAttribute(DirtyAttribute attribute) { + dirtyAttributes.add(attribute); + } + + public boolean isEnlisted() { + return enlisted; + } + + // --- TransactionAware // + + @Override + public Connection getConnection() { + return detachedWrapper(); + } + + @Override + public void transactionStart() throws SQLException { + try { + if (!enlisted && connection.getAutoCommit()) { + connection.setAutoCommit(false); + setDirtyAttribute(AUTOCOMMIT); + } + enlisted = true; + } catch (SQLException se) { + setFlushOnly(se); + throw se; + } + } + + @Override + public void transactionBeforeCompletion(boolean successful) { + if (enlistedOpenWrappers.closeAllAutocloseableElements() != 0) { + if (successful) { + fireOnWarning(connectionPool.getListeners(), "Closing open connection(s) prior to commit"); + } else { + // AG-168 - Close without warning as Synchronization.beforeCompletion is only invoked on success. See issue for more details. + // fireOnWarning( connectionPool.getListeners(), "Closing open connection prior to rollback" ); + } + } + } + + @Override + public void transactionCommit() throws SQLException { + verifyEnlistment(); + try { + connection.commit(); + } catch (SQLException se) { + setFlushOnly(se); + throw se; + } + } + + @Override + public void transactionRollback() throws SQLException { + verifyEnlistment(); + try { + connection.rollback(); + } catch (SQLException se) { + setFlushOnly(se); + throw se; + } + } + + @Override + public void transactionEnd() throws SQLException { + if (enlistedOpenWrappers.closeAllAutocloseableElements() != 0) { + // should never happen, but it's here as a safeguard to prevent double returns in all cases. + fireOnWarning(connectionPool.getListeners(), "Closing open connection(s) on after completion"); + } + enlisted = false; + connectionPool.returnConnectionHandler(this); + } + + @Override + public void transactionCheckCallback(SQLCallable transactionCheck) { + transactionActiveCheck = transactionCheck; + } + + public void verifyEnlistment() throws SQLException { + if (!enlisted && transactionActiveCheck.call()) { + throw new SQLException("Deferred enlistment not supported"); + } + if (enlisted && !transactionActiveCheck.call()) { + throw new SQLException("Enlisted connection used without active transaction"); + } + } + + @Override + public void setFlushOnly() { + // Assumed currentState == State.CHECKED_OUT (or eventually in FLUSH already) + setState(State.FLUSH); + } + + public void setFlushOnly(SQLException se) { + // Assumed currentState == State.CHECKED_OUT (or eventually in FLUSH already) + XbibConnectionPoolConfiguration.ExceptionSorter exceptionSorter = connectionPool.getConfiguration().exceptionSorter(); + if (exceptionSorter != null && exceptionSorter.isFatal(se)) { + setState(State.FLUSH); + } + } + + public enum State { + NEW, CHECKED_IN, CHECKED_OUT, VALIDATION, FLUSH, DESTROYED + } + + public enum DirtyAttribute { + AUTOCOMMIT, TRANSACTION_ISOLATION, NETWORK_TIMEOUT, SCHEMA, CATALOG + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/ConnectionPool.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/ConnectionPool.java new file mode 100644 index 0000000..9856c63 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/ConnectionPool.java @@ -0,0 +1,823 @@ +package org.xbib.jdbc.pool; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.LongAccumulator; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.Function; +import javax.sql.XAConnection; +import org.xbib.jdbc.pool.MetricsRepository.EmptyMetricsRepository; +import org.xbib.jdbc.pool.api.XbibDataSource; +import org.xbib.jdbc.pool.api.XbibDataSourceListener; +import org.xbib.jdbc.pool.api.XbibPoolInterceptor; +import org.xbib.jdbc.pool.api.cache.ConnectionCache; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration; +import org.xbib.jdbc.pool.api.transaction.TransactionIntegration; +import org.xbib.jdbc.pool.util.PriorityScheduledExecutor; +import org.xbib.jdbc.pool.util.StampedCopyOnWriteArrayList; +import org.xbib.jdbc.pool.util.XbibSynchronizer; +import static java.lang.Integer.toHexString; +import static java.lang.System.identityHashCode; +import static java.lang.System.nanoTime; +import static java.lang.Thread.currentThread; +import static java.util.Collections.unmodifiableList; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static org.xbib.jdbc.pool.ConnectionHandler.State.CHECKED_IN; +import static org.xbib.jdbc.pool.ConnectionHandler.State.CHECKED_OUT; +import static org.xbib.jdbc.pool.ConnectionHandler.State.FLUSH; +import static org.xbib.jdbc.pool.ConnectionHandler.State.VALIDATION; +import static org.xbib.jdbc.pool.api.XbibDataSource.FlushMode.GRACEFUL; +import static org.xbib.jdbc.pool.api.XbibDataSource.FlushMode.LEAK; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.MultipleAcquisitionAction.OFF; +import static org.xbib.jdbc.pool.util.InterceptorHelper.fireOnConnectionAcquiredInterceptor; +import static org.xbib.jdbc.pool.util.InterceptorHelper.fireOnConnectionCreateInterceptor; +import static org.xbib.jdbc.pool.util.InterceptorHelper.fireOnConnectionDestroyInterceptor; +import static org.xbib.jdbc.pool.util.InterceptorHelper.fireOnConnectionReturnInterceptor; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionAcquire; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionCreation; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionDestroy; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionFlush; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionLeak; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionReap; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionReturn; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionValidation; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionAcquired; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionCreation; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionDestroy; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionFlush; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionInvalid; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionLeak; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionPooled; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionReap; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionReturn; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionValid; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnInfo; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnWarning; + +public final class ConnectionPool implements Pool { + + private static final AtomicInteger HOUSEKEEP_COUNT = new AtomicInteger(); + + private final XbibConnectionPoolConfiguration configuration; + private final XbibDataSourceListener[] listeners; + + private final StampedCopyOnWriteArrayList allConnections; + + private final XbibSynchronizer synchronizer; + private final ConnectionFactory connectionFactory; + private final PriorityScheduledExecutor housekeepingExecutor; + private final TransactionIntegration transactionIntegration; + + private final boolean borrowValidationEnabled; + private final boolean idleValidationEnabled; + private final boolean leakEnabled; + private final boolean validationEnabled; + private final boolean reapEnabled; + + private final LongAccumulator maxUsed = new LongAccumulator(Math::max, Long.MIN_VALUE); + private final LongAdder activeCount = new LongAdder(); + private final ConnectionCache localCache; + private MetricsRepository metricsRepository; + private List interceptors; + + public ConnectionPool(XbibConnectionPoolConfiguration configuration, XbibDataSourceListener... listeners) { + this.configuration = configuration; + this.listeners = listeners; + + allConnections = new StampedCopyOnWriteArrayList<>(ConnectionHandler.class); + localCache = configuration.connectionCache(); + + synchronizer = new XbibSynchronizer(); + connectionFactory = new ConnectionFactory(configuration.connectionFactoryConfiguration(), listeners); + housekeepingExecutor = new PriorityScheduledExecutor(1, "xbib-" + HOUSEKEEP_COUNT.incrementAndGet(), listeners); + transactionIntegration = configuration.transactionIntegration(); + + borrowValidationEnabled = configuration.validateOnBorrow(); + idleValidationEnabled = !configuration.validateOnBorrow() && !configuration.idleValidationTimeout().isZero(); + leakEnabled = !configuration.leakTimeout().isZero(); + validationEnabled = !configuration.validationTimeout().isZero(); + reapEnabled = !configuration.reapTimeout().isZero(); + } + + public void init() { + if (configuration.acquisitionTimeout().compareTo(configuration.connectionFactoryConfiguration().loginTimeout()) < 0) { + fireOnWarning(listeners, "Login timeout should be smaller than acquisition timeout"); + } + + if (leakEnabled) { + housekeepingExecutor.schedule(new LeakTask(), configuration.leakTimeout().toNanos(), NANOSECONDS); + } + if (validationEnabled) { + housekeepingExecutor.schedule(new ValidationTask(), configuration.validationTimeout().toNanos(), NANOSECONDS); + } + if (reapEnabled) { + housekeepingExecutor.schedule(new ReapTask(), configuration.reapTimeout().toNanos(), NANOSECONDS); + } + transactionIntegration.addResourceRecoveryFactory(connectionFactory.hasRecoveryCredentials() ? connectionFactory : this); + + // fill to the initial size + if (configuration.initialSize() < configuration.minSize()) { + fireOnInfo(listeners, "Initial size smaller than min. Connections will be created when necessary"); + } else if (configuration.initialSize() > configuration.maxSize()) { + fireOnInfo(listeners, "Initial size bigger than max. Connections will be destroyed as soon as they return to the pool"); + } + for (int n = configuration.initialSize(); n > 0; n--) { + housekeepingExecutor.executeNow(new CreateConnectionTask().initial()); + } + } + + public XbibConnectionPoolConfiguration getConfiguration() { + return configuration; + } + + public int defaultJdbcIsolationLevel() { + return connectionFactory.defaultJdbcIsolationLevel(); + } + + public XbibDataSourceListener[] getListeners() { + return listeners; + } + + public List getPoolInterceptors() { + return unmodifiableList(interceptors); + } + + public void setPoolInterceptors(Collection list) { + if (list.stream().anyMatch(i -> i.getPriority() < 0)) { + throw new IllegalArgumentException("Negative priority values on XbibPoolInterceptor are reserved."); + } + if (list.isEmpty() && (interceptors == null || interceptors.isEmpty())) { + return; + } + + interceptors = list.stream().sorted(XbibPoolInterceptor.DEFAULT_COMPARATOR).collect(toList()); + + Function interceptorName = i -> i.getClass().getName() + "@" + toHexString(identityHashCode(i)) + " (priority " + i.getPriority() + ")"; + fireOnInfo(listeners, "Pool interceptors: " + interceptors.stream().map(interceptorName).collect(joining(" >>> ", "[", "]"))); + } + + public void flushPool(XbibDataSource.FlushMode mode) { + if (mode == LEAK && !leakEnabled) { + fireOnWarning(listeners, "Flushing leak connections with no specified leak timout."); + return; + } + housekeepingExecutor.execute(new FlushTask(mode)); + } + + @Override + public void close() { + transactionIntegration.removeResourceRecoveryFactory(connectionFactory.hasRecoveryCredentials() ? connectionFactory : this); + + for (Runnable task : housekeepingExecutor.shutdownNow()) { + if (task instanceof DestroyConnectionTask) { + task.run(); + } + } + + for (ConnectionHandler handler : allConnections) { + handler.setState(FLUSH); + new DestroyConnectionTask(handler).run(); + } + allConnections.clear(); + activeCount.reset(); + + synchronizer.release(synchronizer.getQueueLength()); + } + + @Override + public boolean isRecoverable() { + return connectionFactory.isRecoverable(); + } + + @Override + public XAConnection getRecoveryConnection() throws SQLException { + long stamp = beforeAcquire(); + checkMultipleAcquisition(); + ConnectionHandler checkedOutHandler = null; + + try { + do { + checkedOutHandler = (ConnectionHandler) localCache.get(); + if (checkedOutHandler == null) { + checkedOutHandler = handlerFromSharedCache(); + } + } while ((borrowValidationEnabled && !borrowValidation(checkedOutHandler)) + || (idleValidationEnabled && !idleValidation(checkedOutHandler))); + + activeCount.increment(); + fireOnConnectionAcquiredInterceptor(interceptors, checkedOutHandler); + afterAcquire(stamp, checkedOutHandler, false); + return checkedOutHandler.xaConnectionWrapper(); + } catch (Throwable t) { + if (checkedOutHandler != null) { + checkedOutHandler.setState(CHECKED_OUT, CHECKED_IN); + } + throw t; + } + } + + private long beforeAcquire() throws SQLException { + fireBeforeConnectionAcquire(listeners); + if (housekeepingExecutor.isShutdown()) { + throw new SQLException("This pool is closed and does not handle any more connections!"); + } + return metricsRepository.beforeConnectionAcquire(); + } + + private void checkMultipleAcquisition() throws SQLException { + if (configuration.multipleAcquisition() != OFF) { + for (ConnectionHandler handler : allConnections) { + if (handler.getHoldingThread() == currentThread()) { + switch (configuration.multipleAcquisition()) { + case STRICT: + throw new SQLException("Acquisition of multiple connections by the same Thread."); + case WARN: + fireOnWarning(listeners, "Acquisition of multiple connections by the same Thread. This can lead to pool exhaustion and eventually a deadlock!"); + } + break; + } + } + } + } + + @Override + public Connection getConnection() throws SQLException { + long stamp = beforeAcquire(); + + ConnectionHandler checkedOutHandler = handlerFromTransaction(); + if (checkedOutHandler != null) { + // AG-140 - If associate throws here is fine, it's assumed the synchronization that returns the connection has been registered + transactionIntegration.associate(checkedOutHandler, checkedOutHandler.getXaResource()); + afterAcquire(stamp, checkedOutHandler, true); + return checkedOutHandler.connectionWrapper(); + } + checkMultipleAcquisition(); + + try { + do { + checkedOutHandler = (ConnectionHandler) localCache.get(); + if (checkedOutHandler == null) { + checkedOutHandler = handlerFromSharedCache(); + } + } while ((borrowValidationEnabled && !borrowValidation(checkedOutHandler)) + || (idleValidationEnabled && !idleValidation(checkedOutHandler))); + transactionIntegration.associate(checkedOutHandler, checkedOutHandler.getXaResource()); + + activeCount.increment(); + fireOnConnectionAcquiredInterceptor(interceptors, checkedOutHandler); + afterAcquire(stamp, checkedOutHandler, true); + return checkedOutHandler.connectionWrapper(); + } catch (Throwable t) { + if (checkedOutHandler != null) { + // AG-140 - Return the connection to the pool to prevent leak + checkedOutHandler.setState(CHECKED_OUT, CHECKED_IN); + } + throw t; + } + } + + private ConnectionHandler handlerFromTransaction() throws SQLException { + return (ConnectionHandler) transactionIntegration.getTransactionAware(); + } + + private ConnectionHandler handlerFromSharedCache() throws SQLException { + long remaining = configuration.acquisitionTimeout().toNanos(); + remaining = remaining > 0 ? remaining : Long.MAX_VALUE; + Future task = null; + try { + for (; ; ) { + // If min-size increases, create a connection right away + if (allConnections.size() < configuration.minSize()) { + task = housekeepingExecutor.executeNow(new CreateConnectionTask()); + } + // Try to find an available connection in the pool + for (ConnectionHandler handler : allConnections) { + if (handler.acquire()) { + return handler; + } + } + // If no connections are available and there is room, create one + if (task == null && allConnections.size() < configuration.maxSize()) { + task = housekeepingExecutor.executeNow(new CreateConnectionTask()); + } + long start = nanoTime(); + if (task == null) { + // Pool full, will have to wait for a connection to be returned + if (!synchronizer.tryAcquireNanos(synchronizer.getStamp(), remaining)) { + throw new SQLException("Sorry, acquisition timeout!"); + } + } else { + // Wait for the new connection instead of the synchronizer to propagate any exception on connection establishment + ConnectionHandler handler = task.get(remaining, NANOSECONDS); + if (handler != null && handler.acquire()) { + return handler; + } + task = null; + } + remaining -= nanoTime() - start; + } + } catch (InterruptedException e) { + currentThread().interrupt(); + throw new SQLException("Interrupted while acquiring"); + } catch (ExecutionException e) { + throw unwrapExecutionException(e); + } catch (RejectedExecutionException | CancellationException e) { + throw new SQLException("Can't create new connection as the pool is shutting down", e); + } catch (TimeoutException e) { + task.cancel(true); + // AG-201: Last effort. Connections may have returned to the pool while waiting. + for (ConnectionHandler handler : allConnections) { + if (handler.acquire()) { + return handler; + } + } + throw new SQLException("Acquisition timeout while waiting for new connection", e); + } + } + + private SQLException unwrapExecutionException(ExecutionException ee) { + try { + throw ee.getCause(); + } catch (RuntimeException | Error re) { + throw re; + } catch (SQLException se) { + return se; + } catch (Throwable t) { + return new SQLException("Exception while creating new connection", t); + } + } + + private boolean idleValidation(ConnectionHandler handler) { + if (!handler.isIdle(configuration.idleValidationTimeout())) { + return true; + } + return borrowValidation(handler); + } + + private boolean borrowValidation(ConnectionHandler handler) { + if (handler.setState(CHECKED_OUT, VALIDATION)) { + return performValidation(handler, CHECKED_OUT); + } + return false; + } + + private boolean performValidation(ConnectionHandler handler, ConnectionHandler.State targetState) { + fireBeforeConnectionValidation(listeners, handler); + if (handler.isValid() && handler.setState(VALIDATION, targetState)) { + fireOnConnectionValid(listeners, handler); + synchronizer.releaseConditional(); + return true; + } else { + removeFromPool(handler); + metricsRepository.afterConnectionInvalid(); + fireOnConnectionInvalid(listeners, handler); + return false; + } + } + + private void afterAcquire(long metricsStamp, ConnectionHandler checkedOutHandler, boolean verifyEnlistment) throws SQLException { + metricsRepository.afterConnectionAcquire(metricsStamp); + fireOnConnectionAcquired(listeners, checkedOutHandler); + + if (verifyEnlistment && !checkedOutHandler.isEnlisted()) { + switch (configuration.transactionRequirement()) { + case STRICT: + returnConnectionHandler(checkedOutHandler); + throw new SQLException("Connection acquired without transaction."); + case WARN: + fireOnWarning(listeners, new SQLException("Connection acquired without transaction.")); + } + } + if (leakEnabled || reapEnabled) { + checkedOutHandler.touch(); + } + if (leakEnabled || configuration.multipleAcquisition() != OFF) { + if (checkedOutHandler.getHoldingThread() != null && checkedOutHandler.getHoldingThread() != currentThread()) { + Throwable warn = new Throwable("Shared connection between threads '" + checkedOutHandler.getHoldingThread().getName() + "' and '" + currentThread().getName() + "'"); + warn.setStackTrace(checkedOutHandler.getHoldingThread().getStackTrace()); + fireOnWarning(listeners, warn); + } + checkedOutHandler.setHoldingThread(currentThread()); + if (configuration.enhancedLeakReport()) { + checkedOutHandler.setAcquisitionStackTrace(currentThread().getStackTrace()); + } + } + } + + // --- // + + public void returnConnectionHandler(ConnectionHandler handler) throws SQLException { + fireBeforeConnectionReturn(listeners, handler); + if (leakEnabled) { + handler.setHoldingThread(null); + if (configuration.enhancedLeakReport()) { + handler.setAcquisitionStackTrace(null); + } + } + if (idleValidationEnabled || reapEnabled) { + handler.touch(); + } + try { + if (!transactionIntegration.disassociate(handler)) { + return; + } + } catch (Throwable ignored) { + } + + activeCount.decrement(); + + // resize on change of max-size, or flush on close + int currentSize = allConnections.size(); + if ((currentSize > configuration.maxSize() && currentSize > configuration.minSize()) || configuration.flushOnClose()) { + handler.setState(FLUSH); + removeFromPool(handler); + metricsRepository.afterConnectionReap(); + fireOnConnectionReap(listeners, handler); + return; + } + + try { + handler.resetConnection(); + } catch (SQLException sqlException) { + fireOnWarning(listeners, sqlException); + } + localCache.put(handler); + fireOnConnectionReturnInterceptor(interceptors, handler); + + if (handler.setState(CHECKED_OUT, CHECKED_IN)) { + // here the handler is already up for grabs + synchronizer.releaseConditional(); + metricsRepository.afterConnectionReturn(); + fireOnConnectionReturn(listeners, handler); + } else { + // handler not in CHECKED_OUT implies FLUSH + removeFromPool(handler); + metricsRepository.afterConnectionFlush(); + fireOnConnectionFlush(listeners, handler); + } + } + + private void removeFromPool(ConnectionHandler handler) { + allConnections.remove(handler); + synchronizer.releaseConditional(); + housekeepingExecutor.execute(new FillTask()); + housekeepingExecutor.execute(new DestroyConnectionTask(handler)); + } + + // --- Exposed statistics // + + @Override + public void onMetricsEnabled(boolean metricsEnabled) { + metricsRepository = metricsEnabled ? new DefaultMetricsRepository(this) : new EmptyMetricsRepository(); + } + + public MetricsRepository getMetrics() { + return metricsRepository; + } + + public long activeCount() { + return activeCount.sum(); + } + + public long availableCount() { + return allConnections.size() - activeCount.sum(); + } + + public long maxUsedCount() { + return maxUsed.get(); + } + + public void resetMaxUsedCount() { + maxUsed.reset(); + } + + public long awaitingCount() { + return synchronizer.getQueueLength(); + } + + // --- health check // + + @Override + public boolean isHealthy(boolean newConnection) throws SQLException { + ConnectionHandler healthHandler; + Future task = null; + if (newConnection) { + try { + do { + task = housekeepingExecutor.executeNow(new CreateConnectionTask().initial()); + healthHandler = task.get(configuration.acquisitionTimeout().isZero() ? Long.MAX_VALUE : configuration.acquisitionTimeout().toNanos(), NANOSECONDS); + } while (!healthHandler.setState(CHECKED_IN, VALIDATION)); + } catch (InterruptedException e) { + currentThread().interrupt(); + throw new SQLException("Interrupted while acquiring"); + } catch (ExecutionException ee) { + throw unwrapExecutionException(ee); + } catch (RejectedExecutionException | CancellationException e) { + throw new SQLException("Can't create new connection as the pool is shutting down", e); + } catch (TimeoutException e) { + task.cancel(true); + throw new SQLException("Acquisition timeout on health check"); + } + } else { + healthHandler = handlerFromSharedCache(); + healthHandler.setState(CHECKED_OUT, VALIDATION); + } + return performValidation(healthHandler, CHECKED_IN); + } + + // --- create // + + private final class CreateConnectionTask implements Callable { + + private boolean initial; + + // initial connections do not take configuration.maxSize into account + private CreateConnectionTask initial() { + initial = true; + return this; + } + + @Override + public ConnectionHandler call() throws SQLException { + if (!initial && allConnections.size() >= configuration.maxSize()) { + return null; + } + fireBeforeConnectionCreation(listeners); + long metricsStamp = metricsRepository.beforeConnectionCreation(); + + try { + ConnectionHandler handler = new ConnectionHandler(connectionFactory.createConnection(), ConnectionPool.this); + metricsRepository.afterConnectionCreation(metricsStamp); + + if (!configuration.maxLifetime().isZero()) { + handler.setMaxLifetimeTask(housekeepingExecutor.schedule(new FlushTask(GRACEFUL, handler), configuration.maxLifetime().toNanos(), NANOSECONDS)); + } + + fireOnConnectionCreation(listeners, handler); + fireOnConnectionCreateInterceptor(interceptors, handler); + + handler.setState(CHECKED_IN); + allConnections.add(handler); + + maxUsed.accumulate(allConnections.size()); + fireOnConnectionPooled(listeners, handler); + + return handler; + } catch (SQLException e) { + fireOnWarning(listeners, e); + throw e; + } catch (Throwable t) { + fireOnWarning(listeners, "Failed to create connection due to " + t.getClass().getSimpleName()); + throw t; + } finally { + // not strictly needed, but not harmful either + synchronizer.releaseConditional(); + } + } + } + + // --- flush // + + private final class FlushTask implements Runnable { + + private final XbibDataSource.FlushMode mode; + private final ConnectionHandler handler; + + @SuppressWarnings("WeakerAccess") + FlushTask(XbibDataSource.FlushMode mode) { + this.mode = mode; + this.handler = null; + } + + @SuppressWarnings({"WeakerAccess", "SameParameterValue"}) + FlushTask(XbibDataSource.FlushMode mode, ConnectionHandler handler) { + this.mode = mode; + this.handler = handler; + } + + @Override + public void run() { + for (ConnectionHandler ch : handler != null ? Collections.singleton(handler) : allConnections) { + fireBeforeConnectionFlush(listeners, ch); + flush(mode, ch); + } + afterFlush(mode); + } + + private void flush(XbibDataSource.FlushMode mode, ConnectionHandler handler) { + switch (mode) { + case ALL: + handler.setState(FLUSH); + flushHandler(handler); + break; + case GRACEFUL: + if (handler.setState(CHECKED_IN, FLUSH)) { + flushHandler(handler); + } else if (!handler.setState(CHECKED_OUT, FLUSH) && handler.isAcquirable()) { + // concurrency caused both transitions fail but handler is still acquirable. re-schedule this task. + housekeepingExecutor.execute(this); + } + break; + case LEAK: + if (handler.isLeak(configuration.leakTimeout()) && handler.setState(CHECKED_OUT, FLUSH)) { + flushHandler(handler); + } + break; + case IDLE: + if (allConnections.size() > configuration.minSize() && handler.setState(CHECKED_IN, FLUSH)) { + flushHandler(handler); + } + break; + case INVALID: + fireBeforeConnectionValidation(listeners, handler); + if (handler.setState(CHECKED_IN, VALIDATION)) { + if (handler.isValid() && handler.setState(VALIDATION, CHECKED_IN)) { + fireOnConnectionValid(listeners, handler); + } else { + handler.setState(VALIDATION, FLUSH); + fireOnConnectionInvalid(listeners, handler); + flushHandler(handler); + } + } + break; + default: + } + } + + private void flushHandler(ConnectionHandler handler) { + allConnections.remove(handler); + synchronizer.releaseConditional(); + metricsRepository.afterConnectionFlush(); + fireOnConnectionFlush(listeners, handler); + housekeepingExecutor.execute(new DestroyConnectionTask(handler)); + } + + private void afterFlush(XbibDataSource.FlushMode mode) { + switch (mode) { + case ALL: + case GRACEFUL: + case INVALID: + case LEAK: + case FILL: + // refill to minSize + housekeepingExecutor.execute(new FillTask()); + break; + case IDLE: + break; + default: + fireOnWarning(listeners, "Unsupported Flush mode " + mode); + } + } + } + + // --- fill task // + + private final class FillTask implements Runnable { + + @Override + public void run() { + for (int n = configuration.minSize() - allConnections.size(); n > 0; n--) { + housekeepingExecutor.executeNow(new CreateConnectionTask()); + } + } + } + + // --- leak detection // + + private final class LeakTask implements Runnable { + + @Override + public void run() { + housekeepingExecutor.schedule(this, configuration.leakTimeout().toNanos(), NANOSECONDS); + + for (ConnectionHandler handler : allConnections) { + housekeepingExecutor.execute(new LeakConnectionTask(handler)); + } + } + + private class LeakConnectionTask implements Runnable { + + private final ConnectionHandler handler; + + LeakConnectionTask(ConnectionHandler handler) { + this.handler = handler; + } + + @Override + public void run() { + fireBeforeConnectionLeak(listeners, handler); + if (handler.isLeak(configuration.leakTimeout())) { + metricsRepository.afterLeakDetection(); + fireOnConnectionLeak(listeners, handler); + } + } + } + } + + // --- validation // + + private final class ValidationTask implements Runnable { + + @Override + public void run() { + housekeepingExecutor.schedule(this, configuration.validationTimeout().toNanos(), NANOSECONDS); + + for (ConnectionHandler handler : allConnections) { + housekeepingExecutor.execute(new ValidateConnectionTask(handler)); + } + } + + private class ValidateConnectionTask implements Runnable { + + private final ConnectionHandler handler; + + ValidateConnectionTask(ConnectionHandler handler) { + this.handler = handler; + } + + @Override + public void run() { + if (handler.setState(CHECKED_IN, VALIDATION)) { + performValidation(handler, CHECKED_IN); + } + } + } + } + + // --- reap // + + private final class ReapTask implements Runnable { + + @Override + public void run() { + housekeepingExecutor.schedule(this, configuration.reapTimeout().toNanos(), NANOSECONDS); + + // reset the thead local cache + localCache.reset(); + + for (ConnectionHandler handler : allConnections) { + housekeepingExecutor.execute(new ReapConnectionTask(handler)); + } + } + + private class ReapConnectionTask implements Runnable { + + private final ConnectionHandler handler; + + ReapConnectionTask(ConnectionHandler handler) { + this.handler = handler; + } + + @Override + public void run() { + fireBeforeConnectionReap(listeners, handler); + if (allConnections.size() > configuration.minSize() && handler.setState(CHECKED_IN, FLUSH)) { + if (handler.isIdle(configuration.reapTimeout())) { + removeFromPool(handler); + metricsRepository.afterConnectionReap(); + fireOnConnectionReap(listeners, handler); + } else { + handler.setState(CHECKED_IN); + // for debug, something like: fireOnWarning( listeners, "Connection " + handler.getConnection() + " used recently. Do not reap!" ); + } + } + } + } + } + + // --- destroy // + + private final class DestroyConnectionTask implements Runnable { + + private final ConnectionHandler handler; + + DestroyConnectionTask(ConnectionHandler handler) { + this.handler = handler; + } + + @Override + public void run() { + fireBeforeConnectionDestroy(listeners, handler); + try { + fireOnConnectionDestroyInterceptor(interceptors, handler); + handler.closeConnection(); + } catch (SQLException e) { + fireOnWarning(listeners, e); + } + metricsRepository.afterConnectionDestroy(); + fireOnConnectionDestroy(listeners, handler); + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/DataSource.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/DataSource.java new file mode 100644 index 0000000..2085f4e --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/DataSource.java @@ -0,0 +1,115 @@ +package org.xbib.jdbc.pool; + +import java.io.IOException; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.util.Collection; +import java.util.List; +import java.util.logging.Logger; +import org.xbib.jdbc.pool.api.XbibDataSource; +import org.xbib.jdbc.pool.api.XbibDataSourceListener; +import org.xbib.jdbc.pool.api.XbibDataSourceMetrics; +import org.xbib.jdbc.pool.api.XbibPoolInterceptor; +import org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration; +import static java.util.Collections.emptyList; + +public final class DataSource implements XbibDataSource { + + private final XbibDataSourceConfiguration configuration; + private final Pool connectionPool; + + public DataSource(XbibDataSourceConfiguration dataSourceConfiguration, XbibDataSourceListener... listeners) { + configuration = dataSourceConfiguration; + if (configuration.dataSourceImplementation() == XbibDataSourceConfiguration.DataSourceImplementation.XBIB_POOLLESS) { + connectionPool = new Poolless(dataSourceConfiguration.connectionPoolConfiguration(), listeners); + } else { + connectionPool = new ConnectionPool(dataSourceConfiguration.connectionPoolConfiguration(), listeners); + } + + dataSourceConfiguration.registerMetricsEnabledListener(connectionPool); + connectionPool.onMetricsEnabled(dataSourceConfiguration.metricsEnabled()); + connectionPool.init(); + } + + @Override + public List getPoolInterceptors() { + return connectionPool.getPoolInterceptors(); + } + + @Override + public void setPoolInterceptors(Collection interceptors) { + connectionPool.setPoolInterceptors(interceptors == null ? emptyList() : interceptors); + } + + @Override + public XbibDataSourceConfiguration getConfiguration() { + return configuration; + } + + @Override + public XbibDataSourceMetrics getMetrics() { + return connectionPool.getMetrics(); + } + + @Override + public void flush(FlushMode mode) { + connectionPool.flushPool(mode); + } + + @Override + public boolean isHealthy(boolean newConnection) throws SQLException { + return connectionPool.isHealthy(newConnection); + } + + @Override + public void close() throws IOException { + connectionPool.close(); + } + + @Override + public Connection getConnection() throws SQLException { + return connectionPool.getConnection(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + throw new SQLException("username and password combination invalid on a pooled data source!"); + } + + @Override + public T unwrap(Class target) throws SQLException { + return target.cast(this); + } + + @Override + public boolean isWrapperFor(Class target) throws SQLException { + return target.isInstance(this); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return null; + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + // no-op + } + + @Override + public int getLoginTimeout() throws SQLException { + return 0; + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + // no-op + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + throw new SQLFeatureNotSupportedException("Not Supported"); + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/DataSourceProvider.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/DataSourceProvider.java new file mode 100644 index 0000000..81527ac --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/DataSourceProvider.java @@ -0,0 +1,19 @@ +package org.xbib.jdbc.pool; + +import org.xbib.jdbc.pool.api.XbibDataSource; +import org.xbib.jdbc.pool.api.XbibDataSourceListener; +import org.xbib.jdbc.pool.api.XbibDataSourceProvider; +import org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration; +import static org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration.DataSourceImplementation.XBIB; +import static org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration.DataSourceImplementation.XBIB_POOLLESS; + +public final class DataSourceProvider implements XbibDataSourceProvider { + + public DataSourceProvider() { + } + + @Override + public XbibDataSource getDataSource(XbibDataSourceConfiguration config, XbibDataSourceListener... listeners) { + return config.dataSourceImplementation() == XBIB || config.dataSourceImplementation() == XBIB_POOLLESS ? new DataSource(config, listeners) : null; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/DefaultMetricsRepository.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/DefaultMetricsRepository.java new file mode 100644 index 0000000..f841632 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/DefaultMetricsRepository.java @@ -0,0 +1,222 @@ +package org.xbib.jdbc.pool; + +import java.text.MessageFormat; +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.atomic.LongAccumulator; +import java.util.concurrent.atomic.LongAdder; +import static java.lang.System.nanoTime; +import static java.time.Duration.ZERO; +import static java.time.Duration.ofNanos; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public final class DefaultMetricsRepository implements MetricsRepository { + + private static final String FORMAT_1 = "Connections: {0} created | {1} invalid | {2} reap | {3} flush | {4} destroyed"; + private static final String FORMAT_2 = "Pool: {0} available | {1} active | {2} max | {3} acquired | {4} returned"; + private static final String FORMAT_3 = "Created duration: {0,number,000.000}ms average | {1}ms max | {2}ms total"; + private static final String FORMAT_4 = "Acquire duration: {0,number,000.000}ms average | {1}ms max | {2}ms total"; + private static final String FORMAT_5 = "Threads awaiting: {0}"; + + private final Pool connectionPool; + private final LongAdder creationCount = new LongAdder(); + private final LongAdder creationTotalTime = new LongAdder(); + private final LongAdder acquireCount = new LongAdder(); + private final LongAdder returnCount = new LongAdder(); + private final LongAdder acquireTotalTime = new LongAdder(); + private final LongAdder leakDetectionCount = new LongAdder(); + private final LongAdder invalidCount = new LongAdder(); + private final LongAdder flushCount = new LongAdder(); + private final LongAdder reapCount = new LongAdder(); + private final LongAdder destroyCount = new LongAdder(); + private final LongAccumulator maxCreatedDuration = new LongAccumulator(Long::max, 0); + private final LongAccumulator maxAcquireDuration = new LongAccumulator(Long::max, 0); + + public DefaultMetricsRepository(Pool pool) { + connectionPool = pool; + } + + @Override + public long beforeConnectionCreation() { + return nanoTime(); + } + + @Override + public void afterConnectionCreation(long timestamp) { + long duration = nanoTime() - timestamp; + creationCount.increment(); + creationTotalTime.add(duration); + maxCreatedDuration.accumulate(duration); + } + + @Override + public long beforeConnectionAcquire() { + return nanoTime(); + } + + @Override + public void afterConnectionAcquire(long timestamp) { + long duration = nanoTime() - timestamp; + acquireCount.increment(); + acquireTotalTime.add(duration); + maxAcquireDuration.accumulate(duration); + } + + @Override + public void afterConnectionReturn() { + returnCount.increment(); + } + + @Override + public void afterLeakDetection() { + leakDetectionCount.increment(); + } + + @Override + public void afterConnectionInvalid() { + invalidCount.increment(); + } + + @Override + public void afterConnectionFlush() { + flushCount.increment(); + } + + @Override + public void afterConnectionReap() { + reapCount.increment(); + } + + @Override + public void afterConnectionDestroy() { + destroyCount.increment(); + } + + // --- // + + @Override + public long creationCount() { + return creationCount.longValue(); + } + + @Override + public Duration creationTimeAverage() { + if (creationCount.longValue() == 0) { + return ZERO; + } + return ofNanos(creationTotalTime.longValue() / creationCount.longValue()); + } + + @Override + public Duration creationTimeMax() { + return ofNanos(maxCreatedDuration.get()); + } + + @Override + public Duration creationTimeTotal() { + return ofNanos(creationTotalTime.longValue()); + } + + @Override + public long acquireCount() { + return acquireCount.longValue(); + } + + @Override + public long leakDetectionCount() { + return leakDetectionCount.longValue(); + } + + @Override + public long invalidCount() { + return invalidCount.longValue(); + } + + @Override + public long flushCount() { + return flushCount.longValue(); + } + + @Override + public long reapCount() { + return reapCount.longValue(); + } + + @Override + public long destroyCount() { + return destroyCount.longValue(); + } + + @Override + public long activeCount() { + return connectionPool.activeCount(); + } + + @Override + public long maxUsedCount() { + return connectionPool.maxUsedCount(); + } + + @Override + public long availableCount() { + return connectionPool.availableCount(); + } + + @Override + public Duration blockingTimeAverage() { + if (acquireCount.longValue() == 0) { + return ZERO; + } + return ofNanos(acquireTotalTime.longValue() / acquireCount.longValue()); + } + + @Override + public Duration blockingTimeMax() { + return ofNanos(maxAcquireDuration.get()); + } + + @Override + public Duration blockingTimeTotal() { + return ofNanos(acquireTotalTime.longValue()); + } + + @Override + public long awaitingCount() { + return connectionPool.awaitingCount(); + } + + // --- // + + @Override + public void reset() { + creationCount.reset(); + creationTotalTime.reset(); + acquireCount.reset(); + acquireTotalTime.reset(); + leakDetectionCount.reset(); + invalidCount.reset(); + + maxCreatedDuration.reset(); + maxAcquireDuration.reset(); + connectionPool.resetMaxUsedCount(); + } + + // --- // + + @Override + public String toString() { + double avgCreationMs = (double) creationTimeAverage().toNanos() / MILLISECONDS.toNanos(1); + double avgBlockingMs = (double) blockingTimeAverage().toNanos() / MILLISECONDS.toNanos(1); + + String nl = System.lineSeparator(); + + StringBuffer buffer = new StringBuffer(500); + buffer.append(nl).append("===").append(nl); + new MessageFormat(FORMAT_1, Locale.ROOT).format(new Object[]{creationCount, invalidCount, reapCount, flushCount, destroyCount}, buffer, null).append(nl); + new MessageFormat(FORMAT_2, Locale.ROOT).format(new Object[]{availableCount(), activeCount(), maxUsedCount(), acquireCount, returnCount}, buffer, null).append(nl); + new MessageFormat(FORMAT_3, Locale.ROOT).format(new Object[]{avgCreationMs, creationTimeMax().toMillis(), creationTimeTotal().toMillis()}, buffer, null).append(nl); + new MessageFormat(FORMAT_4, Locale.ROOT).format(new Object[]{avgBlockingMs, blockingTimeMax().toMillis(), blockingTimeTotal().toMillis()}, buffer, null).append(nl); + new MessageFormat(FORMAT_5, Locale.ROOT).format(new Object[]{awaitingCount()}, buffer, null).append(nl); + return buffer.append("===").toString(); + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/MetricsRepository.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/MetricsRepository.java new file mode 100644 index 0000000..b3d6796 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/MetricsRepository.java @@ -0,0 +1,49 @@ +package org.xbib.jdbc.pool; + +import org.xbib.jdbc.pool.api.XbibDataSourceMetrics; + +public interface MetricsRepository extends XbibDataSourceMetrics { + + default long beforeConnectionCreation() { + return 0; + } + + default void afterConnectionCreation(long timestamp) { + } + + default long beforeConnectionAcquire() { + return 0; + } + + default void afterConnectionAcquire(long timestamp) { + } + + default void afterConnectionReturn() { + } + + default void afterLeakDetection() { + } + + default void afterConnectionInvalid() { + } + + default void afterConnectionFlush() { + } + + default void afterConnectionReap() { + } + + default void afterConnectionDestroy() { + } + + final class EmptyMetricsRepository implements MetricsRepository { + + public EmptyMetricsRepository() { + } + + @Override + public String toString() { + return "Metrics Disabled"; + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/Pool.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/Pool.java new file mode 100644 index 0000000..20c2435 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/Pool.java @@ -0,0 +1,51 @@ +package org.xbib.jdbc.pool; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import org.xbib.jdbc.pool.api.XbibDataSource.FlushMode; +import org.xbib.jdbc.pool.api.XbibDataSourceListener; +import org.xbib.jdbc.pool.api.XbibDataSourceMetrics; +import org.xbib.jdbc.pool.api.XbibPoolInterceptor; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration; +import org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration.MetricsEnabledListener; +import org.xbib.jdbc.pool.api.transaction.TransactionIntegration.ResourceRecoveryFactory; + +public interface Pool extends MetricsEnabledListener, AutoCloseable, ResourceRecoveryFactory { + + void init(); + + Connection getConnection() throws SQLException; + + XbibConnectionPoolConfiguration getConfiguration(); + + XbibDataSourceMetrics getMetrics(); + + XbibDataSourceListener[] getListeners(); + + List getPoolInterceptors(); + + void setPoolInterceptors(Collection list); + + void returnConnectionHandler(ConnectionHandler handler) throws SQLException; + + void flushPool(FlushMode mode); + + boolean isHealthy(boolean newConnection) throws SQLException; + + int defaultJdbcIsolationLevel(); + + @Override + void close(); + + long activeCount(); + + long maxUsedCount(); + + long availableCount(); + + long awaitingCount(); + + void resetMaxUsedCount(); +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/Poolless.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/Poolless.java new file mode 100644 index 0000000..1fa649c --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/Poolless.java @@ -0,0 +1,418 @@ +package org.xbib.jdbc.pool; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.LongAccumulator; +import java.util.function.Function; +import javax.sql.XAConnection; +import org.xbib.jdbc.pool.MetricsRepository.EmptyMetricsRepository; +import org.xbib.jdbc.pool.api.XbibDataSource; +import org.xbib.jdbc.pool.api.XbibDataSourceListener; +import org.xbib.jdbc.pool.api.XbibPoolInterceptor; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration; +import org.xbib.jdbc.pool.api.transaction.TransactionIntegration; +import org.xbib.jdbc.pool.util.StampedCopyOnWriteArrayList; +import org.xbib.jdbc.pool.util.XbibSynchronizer; +import static java.lang.Integer.toHexString; +import static java.lang.System.identityHashCode; +import static java.lang.System.nanoTime; +import static java.lang.Thread.currentThread; +import static java.util.Arrays.copyOfRange; +import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static org.xbib.jdbc.pool.ConnectionHandler.State.CHECKED_OUT; +import static org.xbib.jdbc.pool.ConnectionHandler.State.FLUSH; +import static org.xbib.jdbc.pool.ConnectionHandler.State.VALIDATION; +import static org.xbib.jdbc.pool.api.XbibDataSource.FlushMode.ALL; +import static org.xbib.jdbc.pool.api.XbibDataSource.FlushMode.LEAK; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.MultipleAcquisitionAction.OFF; +import static org.xbib.jdbc.pool.util.InterceptorHelper.fireOnConnectionAcquiredInterceptor; +import static org.xbib.jdbc.pool.util.InterceptorHelper.fireOnConnectionCreateInterceptor; +import static org.xbib.jdbc.pool.util.InterceptorHelper.fireOnConnectionDestroyInterceptor; +import static org.xbib.jdbc.pool.util.InterceptorHelper.fireOnConnectionReturnInterceptor; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionAcquire; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionCreation; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionDestroy; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionFlush; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionReturn; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireBeforeConnectionValidation; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionAcquired; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionCreation; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionDestroy; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionFlush; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionInvalid; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionPooled; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnConnectionValid; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnInfo; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnWarning; + +/** + * Alternative implementation of ConnectionPool for the special case of flush-on-close (and min-size == 0) + * In particular, this removes the need for the executor. + * Also, there is no thread-local connection cache as connections are not reused + */ +public final class Poolless implements Pool { + + private final XbibConnectionPoolConfiguration configuration; + private final XbibDataSourceListener[] listeners; + + private final StampedCopyOnWriteArrayList allConnections; + + private final XbibSynchronizer synchronizer; + private final ConnectionFactory connectionFactory; + private final TransactionIntegration transactionIntegration; + + private final LongAccumulator maxUsed = new LongAccumulator(Math::max, Long.MIN_VALUE); + private final AtomicInteger activeCount = new AtomicInteger(); + + private List interceptors; + private MetricsRepository metricsRepository; + private volatile boolean shutdown; + + public Poolless(XbibConnectionPoolConfiguration configuration, XbibDataSourceListener... listeners) { + this.configuration = configuration; + this.listeners = listeners; + + allConnections = new StampedCopyOnWriteArrayList<>(ConnectionHandler.class); + + synchronizer = new XbibSynchronizer(); + connectionFactory = new ConnectionFactory(configuration.connectionFactoryConfiguration(), listeners); + transactionIntegration = configuration.transactionIntegration(); + } + + public void init() { + if (!configuration.maxLifetime().isZero()) { + fireOnInfo(listeners, "Max lifetime not supported in pool-less mode"); + } + if (configuration.validateOnBorrow()) { + fireOnInfo(listeners, "Validation on borrow is not supported in pool-less mode"); + } + if (!configuration.idleValidationTimeout().isZero()) { + fireOnInfo(listeners, "Idle validation not supported in pool-less mode"); + } + if (!configuration.leakTimeout().isZero()) { + fireOnInfo(listeners, "Leak detection not pro-active in pool-less mode"); + } + if (!configuration.reapTimeout().isZero()) { + fireOnInfo(listeners, "Connection reap not supported in pool-less mode"); + } + if (configuration.initialSize() != 0) { + fireOnInfo(listeners, "Initial size is zero in pool-less mode"); + } + if (configuration.minSize() != 0) { + fireOnInfo(listeners, "Min size always zero in pool-less mode"); + } + transactionIntegration.addResourceRecoveryFactory(connectionFactory.hasRecoveryCredentials() ? connectionFactory : this); + } + + public XbibConnectionPoolConfiguration getConfiguration() { + return configuration; + } + + public int defaultJdbcIsolationLevel() { + return connectionFactory.defaultJdbcIsolationLevel(); + } + + public XbibDataSourceListener[] getListeners() { + return listeners; + } + + public List getPoolInterceptors() { + return unmodifiableList(interceptors); + } + + @SuppressWarnings({"StringConcatenation", "SingleCharacterStringConcatenation"}) + public void setPoolInterceptors(Collection list) { + if (list.stream().anyMatch(i -> i.getPriority() < 0)) { + throw new IllegalArgumentException("Negative priority values on XbibPoolInterceptor are reserved."); + } + if (list.isEmpty() && (interceptors == null || interceptors.isEmpty())) { + return; + } + interceptors = list.stream().sorted(XbibPoolInterceptor.DEFAULT_COMPARATOR).collect(toList()); + + Function interceptorName = i -> i.getClass().getName() + "@" + toHexString(identityHashCode(i)) + " (priority " + i.getPriority() + ")"; + fireOnInfo(listeners, "Pool interceptors: " + interceptors.stream().map(interceptorName).collect(joining(" >>> ", "[", "]"))); + } + + // --- // + + @Override + public boolean isRecoverable() { + return connectionFactory.isRecoverable(); + } + + @Override + public XAConnection getRecoveryConnection() throws SQLException { + long stamp = beforeAcquire(); + checkMultipleAcquisition(); + + ConnectionHandler checkedOutHandler = handlerFromSharedCache(); + fireOnConnectionAcquiredInterceptor(interceptors, checkedOutHandler); + afterAcquire(stamp, checkedOutHandler, false); + return checkedOutHandler.xaConnectionWrapper(); + } + + @Override + public void close() { + transactionIntegration.removeResourceRecoveryFactory(connectionFactory.hasRecoveryCredentials() ? connectionFactory : this); + shutdown = true; + + for (ConnectionHandler handler : allConnections) { + handler.setState(FLUSH); + destroyConnection(handler); + } + allConnections.clear(); + + synchronizer.release(synchronizer.getQueueLength()); + } + + private long beforeAcquire() throws SQLException { + fireBeforeConnectionAcquire(listeners); + if (shutdown) { + throw new SQLException("This pool is closed and does not handle any more connections!"); + } + return metricsRepository.beforeConnectionAcquire(); + } + + private void checkMultipleAcquisition() throws SQLException { + if (configuration.multipleAcquisition() != OFF) { + for (ConnectionHandler handler : allConnections) { + if (handler.getHoldingThread() == currentThread()) { + switch (configuration.multipleAcquisition()) { + case STRICT: + throw new SQLException("Acquisition of multiple connections by the same Thread."); + case WARN: + fireOnWarning(listeners, "Acquisition of multiple connections by the same Thread. This can lead to pool exhaustion and eventually a deadlock!"); + } + break; + } + } + } + } + + public Connection getConnection() throws SQLException { + long stamp = beforeAcquire(); + + ConnectionHandler checkedOutHandler = handlerFromTransaction(); + if (checkedOutHandler != null) { + // AG-140 - If associate throws here is fine, it's assumed the synchronization that returns the connection has been registered + transactionIntegration.associate(checkedOutHandler, checkedOutHandler.getXaResource()); + afterAcquire(stamp, checkedOutHandler, false); + return checkedOutHandler.connectionWrapper(); + } + checkMultipleAcquisition(); + + try { + checkedOutHandler = handlerFromSharedCache(); + transactionIntegration.associate(checkedOutHandler, checkedOutHandler.getXaResource()); + fireOnConnectionAcquiredInterceptor(interceptors, checkedOutHandler); + afterAcquire(stamp, checkedOutHandler, true); + return checkedOutHandler.connectionWrapper(); + } catch (Throwable t) { + if (checkedOutHandler != null) { + // AG-140 - Flush handler to prevent leak + flushHandler(checkedOutHandler); + } + throw t; + } + } + + private ConnectionHandler handlerFromTransaction() throws SQLException { + return (ConnectionHandler) transactionIntegration.getTransactionAware(); + } + + private ConnectionHandler handlerFromSharedCache() throws SQLException { + long remaining = configuration.acquisitionTimeout().toNanos(); + remaining = remaining > 0 ? remaining : Long.MAX_VALUE; + try { + for (; ; ) { + // Try to get a "token" to create a new connection + if (activeCount.incrementAndGet() <= configuration.maxSize()) { + try { + return createConnection(); + } catch (SQLException e) { + activeCount.decrementAndGet(); + throw e; + } + } else { + activeCount.decrementAndGet(); + } + + // Pool full, will have to wait for a connection to be returned + long synchronizationStamp = synchronizer.getStamp(); + long start = nanoTime(); + if (remaining < 0 || !synchronizer.tryAcquireNanos(synchronizationStamp, remaining)) { + throw new SQLException("Sorry, acquisition timeout!"); + } + if (shutdown) { + throw new SQLException("Can't create new connection as the pool is shutting down"); + } + remaining -= nanoTime() - start; + } + } catch (InterruptedException e) { + currentThread().interrupt(); + throw new SQLException("Interrupted while acquiring"); + } + } + + @SuppressWarnings("SingleCharacterStringConcatenation") + private void afterAcquire(long metricsStamp, ConnectionHandler checkedOutHandler, boolean verifyEnlistment) throws SQLException { + metricsRepository.afterConnectionAcquire(metricsStamp); + fireOnConnectionAcquired(listeners, checkedOutHandler); + + if (verifyEnlistment && !checkedOutHandler.isEnlisted()) { + switch (configuration.transactionRequirement()) { + case STRICT: + returnConnectionHandler(checkedOutHandler); + throw new SQLException("Connection acquired without transaction."); + case WARN: + fireOnWarning(listeners, new SQLException("Connection acquired without transaction.")); + } + } + if (!configuration.leakTimeout().isZero() || configuration.multipleAcquisition() != OFF) { + if (checkedOutHandler.getHoldingThread() != null && checkedOutHandler.getHoldingThread() != currentThread()) { + Throwable warn = new Throwable("Shared connection between threads '" + checkedOutHandler.getHoldingThread().getName() + "' and '" + currentThread().getName() + "'"); + warn.setStackTrace(checkedOutHandler.getHoldingThread().getStackTrace()); + fireOnWarning(listeners, warn); + } + checkedOutHandler.setHoldingThread(currentThread()); + checkedOutHandler.touch(); + if (configuration.enhancedLeakReport()) { + StackTraceElement[] stackTrace = currentThread().getStackTrace(); + checkedOutHandler.setAcquisitionStackTrace(copyOfRange(stackTrace, 4, stackTrace.length)); + } + } + } + + public void returnConnectionHandler(ConnectionHandler handler) throws SQLException { + fireBeforeConnectionReturn(listeners, handler); + try { + if (!transactionIntegration.disassociate(handler)) { + return; + } + } catch (Throwable ignored) { + } + + fireOnConnectionReturnInterceptor(interceptors, handler); + flushHandler(handler); + } + + @Override + public void onMetricsEnabled(boolean metricsEnabled) { + metricsRepository = metricsEnabled ? new DefaultMetricsRepository(this) : new EmptyMetricsRepository(); + } + + public MetricsRepository getMetrics() { + return metricsRepository; + } + + public long activeCount() { + return activeCount.get(); + } + + public long availableCount() { + return configuration.maxSize() - activeCount.get(); + } + + public long maxUsedCount() { + return maxUsed.get(); + } + + public void resetMaxUsedCount() { + maxUsed.reset(); + } + + public long awaitingCount() { + return synchronizer.getQueueLength(); + } + + @Override + public boolean isHealthy(boolean newConnection) throws SQLException { + // the main difference here is that connect will not block (and potentially timeout) on full pool + if (newConnection) { + activeCount.incrementAndGet(); + } + ConnectionHandler healthHandler = newConnection ? createConnection() : handlerFromSharedCache(); + + try { + fireBeforeConnectionValidation(listeners, healthHandler); + if (healthHandler.setState(CHECKED_OUT, VALIDATION) && healthHandler.isValid() && healthHandler.setState(VALIDATION, CHECKED_OUT)) { + fireOnConnectionValid(listeners, healthHandler); + return true; + } else { + metricsRepository.afterConnectionInvalid(); + fireOnConnectionInvalid(listeners, healthHandler); + return false; + } + } finally { + returnConnectionHandler(healthHandler); + } + } + + private ConnectionHandler createConnection() throws SQLException { + fireBeforeConnectionCreation(listeners); + long metricsStamp = metricsRepository.beforeConnectionCreation(); + + try { + ConnectionHandler handler = new ConnectionHandler(connectionFactory.createConnection(), this); + metricsRepository.afterConnectionCreation(metricsStamp); + + fireOnConnectionCreation(listeners, handler); + fireOnConnectionCreateInterceptor(interceptors, handler); + + handler.setState(CHECKED_OUT); + allConnections.add(handler); + + maxUsed.accumulate(allConnections.size()); + fireOnConnectionPooled(listeners, handler); + + return handler; + } catch (SQLException e) { + fireOnWarning(listeners, e); + throw e; + } + } + + public void flushPool(XbibDataSource.FlushMode mode) { + if (mode == ALL) { + for (ConnectionHandler handler : allConnections) { + fireBeforeConnectionFlush(listeners, handler); + flushHandler(handler); + } + } else if (mode == LEAK) { + for (ConnectionHandler handler : allConnections) { + if (handler.isLeak(configuration.leakTimeout())) { + fireBeforeConnectionFlush(listeners, handler); + flushHandler(handler); + } + } + } + } + + private void flushHandler(ConnectionHandler handler) { + handler.setState(FLUSH); + allConnections.remove(handler); + activeCount.decrementAndGet(); + synchronizer.releaseConditional(); + metricsRepository.afterConnectionFlush(); + fireOnConnectionFlush(listeners, handler); + destroyConnection(handler); + } + + private void destroyConnection(ConnectionHandler handler) { + fireBeforeConnectionDestroy(listeners, handler); + try { + fireOnConnectionDestroyInterceptor(interceptors, handler); + handler.closeConnection(); + } catch (SQLException e) { + fireOnWarning(listeners, e); + } + metricsRepository.afterConnectionDestroy(); + fireOnConnectionDestroy(listeners, handler); + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSource.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSource.java new file mode 100644 index 0000000..f5f7c9b --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSource.java @@ -0,0 +1,118 @@ +package org.xbib.jdbc.pool.api; + +import java.io.Closeable; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; +import javax.sql.DataSource; +import org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration; +import static java.util.ServiceLoader.load; + +/** + * Extension of the DataSource interface that exposes some of its internals. + */ +public interface XbibDataSource extends DataSource, Closeable { + + /** + * Create an XbibDataSource from a supplier of the configuration. + */ + static XbibDataSource from(Supplier configurationSupplier, XbibDataSourceListener... listeners) throws SQLException { + return from(configurationSupplier.get(), listeners); + } + + /** + * Create an XbibDataSource from configuration. + */ + static XbibDataSource from(XbibDataSourceConfiguration configuration, XbibDataSourceListener... listeners) throws SQLException { + for (XbibDataSourceProvider provider : load(XbibDataSourceProvider.class, XbibDataSourceProvider.class.getClassLoader())) { + XbibDataSource implementation = provider.getDataSource(configuration, listeners); + if (implementation != null) { + return implementation; + } + } + + // Fall back to load the implementation using reflection + try { + Class dataSourceClass = XbibDataSource.class.getClassLoader().loadClass(configuration.dataSourceImplementation().className()).asSubclass(XbibDataSource.class); + Constructor dataSourceConstructor = dataSourceClass.getConstructor(XbibDataSourceConfiguration.class, XbibDataSourceListener[].class); + return dataSourceConstructor.newInstance(configuration, listeners); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | + InstantiationException e) { + throw new SQLException("Could not load the required implementation", e); + } + } + + /** + * Allows inspection of the configuration. Some properties allow read / write. + */ + XbibDataSourceConfiguration getConfiguration(); + + /** + * Allows access to metrics. If metrics are not enabled, returns default values. + */ + XbibDataSourceMetrics getMetrics(); + + /** + * Performs a flush action on the connections of the pool. + */ + void flush(FlushMode mode); + + /** + * Get the list of pool interceptors. Interceptors are sorted from high to low priority. + */ + List getPoolInterceptors(); + + /** + * Sets pool interceptors. + */ + void setPoolInterceptors(Collection interceptors); + + /** + * Performs a health check. The newConnection parameter determines that a new database connection is established for this purpose, otherwise attempts to get a connection from the pool. + *

+ * WARNING: Using a new connection may cause the size of the pool to go over max-size. + */ + default boolean isHealthy(boolean newConnection) throws SQLException { + return true; + } + + /** + * Modes supported on the flush operation. + */ + enum FlushMode { + + /** + * All connections are flushed right away. + */ + ALL, + + /** + * Idle connections are flushed. + */ + IDLE, + + /** + * Performs on-demand validation of idle connections. + */ + INVALID, + + /** + * Active connections are flushed on return. Idle connections are flushed immediately. + */ + GRACEFUL, + + /** + * Flushes connections that have been in use for longer than the specified leak timeout. + */ + LEAK, + + /** + * Creates connections to met the minimum size of the pool. + * Used after and increase of minimum size, to make that change effective immediately. + */ + FILL + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSourceListener.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSourceListener.java new file mode 100644 index 0000000..501db7a --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSourceListener.java @@ -0,0 +1,138 @@ +package org.xbib.jdbc.pool.api; + +import java.sql.Connection; + +/** + * This interface defines a set of callback methods that are invoked on events considered important for the state of the pool. + * It not only allows for logging this events, but also for black-box testing. + * NOTE: implementations of this methods should not block / do expensive operations. + */ +public interface XbibDataSourceListener { + + /** + * This callback is invoked whenever a new connection is about to be created. + */ + default void beforeConnectionCreation() { + } + + /** + * This callback is invoked for every new connection. + */ + default void onConnectionCreation(Connection connection) { + } + + /** + * This callback is invoked right after a connection is added to the pool. The connection may have been acquired concurrently. + */ + default void onConnectionPooled(Connection connection) { + } + + /** + * This callback is invoked whenever an application tries to obtain a connection. + */ + default void beforeConnectionAcquire() { + } + + /** + * This callback is invoked when a connection is successfully acquired. + */ + default void onConnectionAcquire(Connection connection) { + } + + /** + * This callback is invoked before a connection is returned to the pool. + */ + default void beforeConnectionReturn(Connection connection) { + } + + /** + * This callback is invoked right after a connection is returned to the pool. The connection may have been acquired concurrently. + */ + default void onConnectionReturn(Connection connection) { + } + + /** + * This callback is invoked before checking the leak timeout of a connection. + */ + default void beforeConnectionLeak(Connection connection) { + } + + /** + * This connection is invoked when a connection is held for longer than the leak timeout and reports what thread acquired it. + */ + default void onConnectionLeak(Connection connection, Thread thread) { + } + + /** + * This callback is invoked when a connection is about to be checked. + */ + default void beforeConnectionValidation(Connection connection) { + } + + /** + * This callback is invoked when a connection was checked and is valid. + */ + default void onConnectionValid(Connection connection) { + } + + /** + * This callback is invoked when a connection was checked and is invalid. The connection will be destroyed. + */ + default void onConnectionInvalid(Connection connection) { + } + + /** + * This callback is invoked when a connection is about to be flush. It may not be flushed. + */ + default void beforeConnectionFlush(Connection connection) { + } + + /** + * This callback is invoked when after a connection is removed from the pool. + */ + default void onConnectionFlush(Connection connection) { + } + + /** + * This callback is invoked before checking the idle timeout of a connection. + */ + default void beforeConnectionReap(Connection connection) { + } + + /** + * This callback is invoked if a connection is idle in the pool. The connection will be destroyed. + */ + default void onConnectionReap(Connection connection) { + } + + /** + * This callback is invoked whenever a connection is about to be destroyed. + */ + default void beforeConnectionDestroy(Connection connection) { + } + + /** + * This callback is invoked after a connection is closed. + */ + default void onConnectionDestroy(Connection connection) { + } + + /** + * This callback is invoked to report anomalous circumstances that do not prevent the pool from functioning. + */ + default void onWarning(String message) { + } + + /** + * This callback is invoked to report anomalous circumstances that do not prevent the pool from functioning. + */ + default void onWarning(Throwable throwable) { + } + + /** + * Callback to allow reporting information of interest, for which a warning might be considered excessive. + */ + default void onInfo(String message) { + } + +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSourceMetrics.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSourceMetrics.java new file mode 100644 index 0000000..ba63b9d --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSourceMetrics.java @@ -0,0 +1,136 @@ +package org.xbib.jdbc.pool.api; + +import java.time.Duration; + +/** + * Several metrics provided by the pool. + */ +public interface XbibDataSourceMetrics { + + /** + * Number of created connections. + */ + default long creationCount() { + return 0; + } + + /** + * Average time for a connection to be created. + */ + default Duration creationTimeAverage() { + return Duration.ZERO; + } + + /** + * Maximum time for a connection to be created. + */ + default Duration creationTimeMax() { + return Duration.ZERO; + } + + /** + * Total time waiting for a connections to be created. + */ + default Duration creationTimeTotal() { + return Duration.ZERO; + } + + /** + * Number of times a leak was detected. A single connection can be detected multiple times. + */ + default long leakDetectionCount() { + return 0; + } + + /** + * Number of connections removed from the pool for being invalid. + */ + default long invalidCount() { + return 0; + } + + /** + * Number of connections removed from the pool, not counting invalid / idle. + */ + default long flushCount() { + return 0; + } + + /** + * Number of connections removed from the pool for being idle. + */ + default long reapCount() { + return 0; + } + + /** + * Number of destroyed connections. + */ + default long destroyCount() { + return 0; + } + + // --- // + + /** + * Number of active connections. This connections are in use and not available to be acquired. + */ + default long activeCount() { + return 0; + } + + /** + * Maximum number of connections active simultaneously. + */ + default long maxUsedCount() { + return 0; + } + + /** + * Number of idle connections in the pool, available to be acquired. + */ + default long availableCount() { + return 0; + } + + /** + * Number of times an acquire operation succeeded. + */ + default long acquireCount() { + return 0; + } + + /** + * Average time an application waited to acquire a connection. + */ + default Duration blockingTimeAverage() { + return Duration.ZERO; + } + + /** + * Maximum time an application waited to acquire a connection. + */ + default Duration blockingTimeMax() { + return Duration.ZERO; + } + + /** + * Total time applications waited to acquire a connection. + */ + default Duration blockingTimeTotal() { + return Duration.ZERO; + } + + /** + * Approximate number of threads blocked, waiting to acquire a connection. + */ + default long awaitingCount() { + return 0; + } + + /** + * Reset the metrics. + */ + default void reset() { + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSourceProvider.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSourceProvider.java new file mode 100644 index 0000000..428ea67 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibDataSourceProvider.java @@ -0,0 +1,14 @@ +package org.xbib.jdbc.pool.api; + +import org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration; + +/** + * An interface for providers of XbibDataSource. + */ +public interface XbibDataSourceProvider { + + /** + * Factory method for XbibDataSource. This method must return null if it can't create an XbibDataSource based on the supplied configuration. + */ + XbibDataSource getDataSource(XbibDataSourceConfiguration configuration, XbibDataSourceListener... listeners); +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibPoolInterceptor.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibPoolInterceptor.java new file mode 100644 index 0000000..c2ed2b2 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/XbibPoolInterceptor.java @@ -0,0 +1,55 @@ +package org.xbib.jdbc.pool.api; + +import java.sql.Connection; +import java.util.Comparator; + +/** + * Callback interface for pool actions. + * These differ from the Listener in a few different ways: + * They do not have access to the raw Connection + * The invoke order is dependent on the operation (incoming / outgoing) + * Consistent with the transaction. + */ +public interface XbibPoolInterceptor { + + /** + * Uses interceptor priority, followed by {@link Class#getName()} to ensure a consistent ordering. + */ + Comparator DEFAULT_COMPARATOR = + Comparator.comparingInt(XbibPoolInterceptor::getPriority).thenComparing(i -> i.getClass().getName()); + + /** + * This callback is invoked when a new connection is created. + */ + default void onConnectionCreate(Connection connection) { + } + + /** + * This callback is invoked when a connection is successfully acquired. + * When in a transactional environment this is invoked only once for multiple acquire calls within the same transaction, before the connection is associated. + */ + default void onConnectionAcquire(Connection connection) { + } + + /** + * This callback is invoked before a connection is returned to the pool. + * When in a transactional environment this is invoked only once for each transaction, after commit / rollback. + * This callback runs after reset, allowing connections to be in the pool in a different state than the described on the configuration. + */ + default void onConnectionReturn(Connection connection) { + } + + /** + * This callback is invoked just before a connection is destroyed. + */ + default void onConnectionDestroy(Connection connection) { + } + + /** + * Allows a ordering between multiple interceptors. + * Lower priority are executed first. Negative values are reserved. + */ + default int getPriority() { + return 0; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/cache/Acquirable.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/cache/Acquirable.java new file mode 100644 index 0000000..74f14b3 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/cache/Acquirable.java @@ -0,0 +1,21 @@ +package org.xbib.jdbc.pool.api.cache; + +/** + * Contract for objects that can be acquired. + */ +public interface Acquirable { + + /** + * Attempts to acquire this object. + * + * @return true on successful object acquisition, false otherwise. + */ + boolean acquire(); + + /** + * This method signals if the object can't be acquired in future calls to acquire(). + * + * @return true if this object can eventually be acquired in the future. + */ + boolean isAcquirable(); +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/cache/ConnectionCache.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/cache/ConnectionCache.java new file mode 100644 index 0000000..c03cf40 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/cache/ConnectionCache.java @@ -0,0 +1,48 @@ +package org.xbib.jdbc.pool.api.cache; + +/** + * Interface for a cache of connections. It's intended for mapping connections to executing threads. + */ +public interface ConnectionCache { + + /** + * An implementation that does not cache. + */ + static ConnectionCache none() { + return new ConnectionCache() { + @Override + public Acquirable get() { + return null; + } + + @Override + public void put(Acquirable acquirable) { + // nothing to do + } + + @Override + public void reset() { + // nothing to do + } + }; + } + + // --- // + + /** + * Get a acquirable object from cache. + * + * @return a connection successfully acquired, according to {@link Acquirable#acquire()} + */ + Acquirable get(); + + /** + * Cache an acquirable object on this cache. + */ + void put(Acquirable acquirable); + + /** + * Reset the cache. + */ + void reset(); +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/cache/LocalConnectionCache.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/cache/LocalConnectionCache.java new file mode 100644 index 0000000..16ff8a6 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/cache/LocalConnectionCache.java @@ -0,0 +1,135 @@ +package org.xbib.jdbc.pool.api.cache; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Implementations of ConnectionCache that rely on {@link ThreadLocal}. + */ +public interface LocalConnectionCache { + + /** + * A local cache that stores at most a single connection. + */ + static ConnectionCache single() { + return new ConnectionCache() { + + @SuppressWarnings("ThreadLocalNotStaticFinal") + private volatile ThreadLocal threadLocal; + + { // instance initializer + reset(); + } + + @Override + public Acquirable get() { + Acquirable acquirable = threadLocal.get(); + return acquirable != null && acquirable.acquire() ? acquirable : null; + } + + @Override + public void put(Acquirable acquirable) { + if (acquirable.isAcquirable()) { + threadLocal.set(acquirable); + } + } + + @Override + public void reset() { + threadLocal = new ThreadLocal<>(); + } + }; + } + + /** + * A local cache that stores up to a number of connections. + */ + static ConnectionCache fixed(int size) { + return new ConnectionCache() { + + @SuppressWarnings("ThreadLocalNotStaticFinal") + private volatile ThreadLocal threadLocal; + + { // instance initializer + reset(); + } + + @Override + public Acquirable get() { + Acquirable[] cacheArray = threadLocal.get(); + for (int i = cacheArray.length; i > 0; ) { // iterate from last to first + Acquirable element = cacheArray[--i]; + if (element != null) { + cacheArray[i] = null; + if (element.acquire()) { + return element; + } + } + } + return null; + } + + @Override + public void put(Acquirable acquirable) { + if (acquirable.isAcquirable()) { + Acquirable[] cacheArray = threadLocal.get(); + int last = cacheArray.length - 1; + if (cacheArray[last] != null) { // always store in last. if there is a previous entry try to move it to other slot + int i = last; + while (--i > 0 && cacheArray[i] != null) ; // find first free slot + cacheArray[i < 0 ? last - 1 : i] = cacheArray[last]; // if no free slot found, override last -1 + } + cacheArray[last] = acquirable; + } + } + + @Override + public void reset() { + threadLocal = ThreadLocal.withInitial(() -> new Acquirable[size]); + } + }; + } + + /** + * A local cache that stores all connections + */ + static ConnectionCache full() { + return new ConnectionCache() { + + @SuppressWarnings("ThreadLocalNotStaticFinal") + private volatile ThreadLocal> threadLocal; + + { // instance initializer + reset(); + } + + @Override + public Acquirable get() { + Deque queue = threadLocal.get(); + + for (int i = queue.size(); i > 0; i--) { + Acquirable acquirable = queue.removeFirst(); + if (acquirable.acquire()) { + return acquirable; + } else if (acquirable.isAcquirable()) { + queue.addLast(acquirable); + } + } + return null; + } + + @Override + public void put(Acquirable acquirable) { + if (acquirable.acquire()) { + threadLocal.get().addFirst(acquirable); + } + } + + @Override + public void reset() { + // ArrayDeque with default initial capacity of 16 + threadLocal = ThreadLocal.withInitial(ArrayDeque::new); + } + }; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/XbibConnectionFactoryConfiguration.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/XbibConnectionFactoryConfiguration.java new file mode 100644 index 0000000..98b729d --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/XbibConnectionFactoryConfiguration.java @@ -0,0 +1,149 @@ +package org.xbib.jdbc.pool.api.configuration; + +import java.security.Principal; +import java.sql.Connection; +import java.time.Duration; +import java.util.Collection; +import java.util.Properties; +import org.xbib.jdbc.pool.api.security.XbibSecurityProvider; + +/** + * The configuration of the connection factory. + */ +public interface XbibConnectionFactoryConfiguration { + + /** + * If connections should have the auto-commit mode on by default. The transaction integration may disable auto-commit when a connection in enrolled in a transaction. + */ + boolean autoCommit(); + + /** + * If JDBC resources ({@link java.sql.Statement} and {@link java.sql.ResultSet}) should be tracked to be closed if leaked. + */ + boolean trackJdbcResources(); + + /** + * Maximum time to wait while attempting to connect to a database. Resolution in seconds. + */ + Duration loginTimeout(); + + /** + * The database URL to connect to. + */ + String jdbcUrl(); + + /** + * A SQL command to be executed when a connection is created. + */ + String initialSql(); + + /** + * JDBC driver class to use as a supplier of connections. Must be an implementation of {@link java.sql.Driver}, {@link javax.sql.DataSource} or {@link javax.sql.XADataSource}. + * Can be null, in which case the driver will be obtained from the URL (using the {@link java.sql.DriverManager#getDriver(String)} mechanism). + */ + Class connectionProviderClass(); + + /** + * The isolation level between database transactions. + */ + IsolationLevel jdbcTransactionIsolation(); + + /** + * A collection of providers that are capable of handling principal / credential objects + */ + Collection securityProviders(); + + /** + * Entity to be authenticated in the database. + */ + Principal principal(); + + /** + * Collection of evidence used for authentication. + */ + Collection credentials(); + + /** + * Entity to be authenticated in the database for recovery connections. If not set, the principal will be used. + */ + Principal recoveryPrincipal(); + + /** + * Collection of evidence used for authentication for recovery connections. If not set, the credentials will be used. + */ + Collection recoveryCredentials(); + + /** + * Other unspecified properties to be passed into the JDBC driver when creating new connections. + * NOTE: username and password properties are not allowed, these have to be set using the principal / credentials mechanism. + */ + Properties jdbcProperties(); + + /** + * Override of JDBC properties used for XA drivers. If left empty, the regular JDBC properties are used for both XA and non-XA. + */ + Properties xaProperties(); + + // --- // + + /** + * The default transaction isolation levels, defined in {@link Connection}. + */ + enum TransactionIsolation implements IsolationLevel { + UNDEFINED, NONE, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE; + + public static TransactionIsolation fromLevel(int level) { + switch (level) { + case Connection.TRANSACTION_NONE: + return NONE; + case Connection.TRANSACTION_READ_UNCOMMITTED: + return READ_UNCOMMITTED; + case Connection.TRANSACTION_READ_COMMITTED: + return READ_COMMITTED; + case Connection.TRANSACTION_REPEATABLE_READ: + return REPEATABLE_READ; + case Connection.TRANSACTION_SERIALIZABLE: + return SERIALIZABLE; + default: + return UNDEFINED; + } + } + + public int level() { + switch (this) { + case READ_UNCOMMITTED: + return Connection.TRANSACTION_READ_UNCOMMITTED; + case READ_COMMITTED: + return Connection.TRANSACTION_READ_COMMITTED; + case REPEATABLE_READ: + return Connection.TRANSACTION_REPEATABLE_READ; + case SERIALIZABLE: + return Connection.TRANSACTION_SERIALIZABLE; + case NONE: + return Connection.TRANSACTION_NONE; + default: + return -1; + } + } + + public boolean isDefined() { + return this != UNDEFINED; + } + } + + /** + * Interface to define the transaction isolation level. + */ + interface IsolationLevel { + + /** + * If a level is not defined it will not be set by the pool (it will use the JDBC driver default). + */ + boolean isDefined(); + + /** + * The value for transaction isolation level. + */ + int level(); + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/XbibConnectionPoolConfiguration.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/XbibConnectionPoolConfiguration.java new file mode 100644 index 0000000..0cac166 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/XbibConnectionPoolConfiguration.java @@ -0,0 +1,281 @@ +package org.xbib.jdbc.pool.api.configuration; + +import java.sql.Connection; +import java.sql.SQLException; +import java.time.Duration; +import org.xbib.jdbc.pool.api.cache.ConnectionCache; +import org.xbib.jdbc.pool.api.transaction.TransactionIntegration; + +/** + * The configuration of the connection pool. + */ +public interface XbibConnectionPoolConfiguration { + + /** + * Configuration for the connection factory. + */ + XbibConnectionFactoryConfiguration connectionFactoryConfiguration(); + + /** + * The connection validation method. Allows customization of the validation operation. + */ + ConnectionValidator connectionValidator(); + + /** + * Allows a custom exception sorter. This determines if a connection is still usable after an exception. + */ + ExceptionSorter exceptionSorter(); + + /** + * Customizable strategy for connection caching. + */ + ConnectionCache connectionCache(); + + /** + * The transaction layer integration to use. + */ + TransactionIntegration transactionIntegration(); + + /** + * Requires connections to be enlisted into a transaction. + */ + TransactionRequirement transactionRequirement(); + + /** + * Validates connections before being acquired (foreground validation) overriding any idle timeout. + * Because of the overhead of performing validation on every call, it's recommended to rely on idle valdation instead. + */ + boolean validateOnBorrow(); + + /** + * Connections idle for longer than this time period are validated before being acquired (foreground validation). + * A duration of {@link Duration#ZERO} means that this feature is disabled. + */ + Duration idleValidationTimeout(); + + /** + * Connections acquired for longer than this time period may be reported as leaking. + * A duration of {@link Duration#ZERO} means that this feature is disabled. + */ + Duration leakTimeout(); + + /** + * Connections idle for longer than this time period are validated (background validation). + * A duration of {@link Duration#ZERO} means that this feature is disabled. + */ + Duration validationTimeout(); + + /** + * Connections idle for longer than this time period are flushed from the pool. + * A duration of {@link Duration#ZERO} means that this feature is disabled. + */ + Duration reapTimeout(); + + /** + * Connections that are older than this time period are flushed from the pool. + * A duration of {@link Duration#ZERO} means that this feature is disabled. + */ + Duration maxLifetime(); + + /** + * Provides detailed insights of the connection status when it's reported as a leak (as INFO messages on XbibDataSourceListener). + */ + boolean enhancedLeakReport(); + + /** + * If connections should be flushed when returning to the pool. + */ + boolean flushOnClose(); + + /** + * Behaviour when a thread tries to acquire multiple connections. + */ + MultipleAcquisitionAction multipleAcquisition(); + + /** + * The number of connections to be created when the pool starts. Can be smaller than min or bigger than max. + */ + int initialSize(); + + // --- Mutable attributes // + + /** + * The minimum number of connections on the pool. If the pool has to flush connections it may create connections to keep this amount. + */ + int minSize(); + + /** + * Sets a new minimum number of connections on the pool. When this value increase the pool may temporarily have less connections than the minimum. + */ + void setMinSize(int size); + + /** + * The maximum number of connections on the pool. When the number of acquired connections is equal to this value, further requests will block. + */ + int maxSize(); + + /** + * Sets a new maximum number of connections on the pool. When this value decreases the pool may temporarily have more connections than the maximum. + */ + void setMaxSize(int size); + + /** + * The maximum amount of time a thread may be blocked waiting for a connection. If this time expires and still no connection is available, an exception is thrown. + * A duration of {@link Duration#ZERO} means that a thread will wait indefinitely. + * In Pool-less this timeout can add to {@link XbibConnectionFactoryConfiguration#loginTimeout()}. + */ + Duration acquisitionTimeout(); + + /** + * Sets a new amount of time a thread may be blocked. Threads already blocked when this value changes do not see the new value when they unblock. + * A duration of {@link Duration#ZERO} means that a thread will wait indefinitely. + */ + void setAcquisitionTimeout(Duration timeout); + + // --- // + + /** + * Modes available for transaction requirement. + */ + enum TransactionRequirement { + /** + * Enlistment not required. + */ + OFF, + /** + * Warn if not enlisted. + */ + WARN, + /** + * Throw exception if not enlisted. + */ + STRICT + } + + /** + * Action to perform on acquisition of multiple connections by the same thread. + */ + enum MultipleAcquisitionAction { + /** + * No restriction. + */ + OFF, + /** + * Warn if thread already holds a connection. + */ + WARN, + /** + * Enforces single connection by throwing exception. + */ + STRICT + } + + // --- // + + /** + * Interface for custom connection validation strategies. + */ + interface ConnectionValidator { + + /** + * The default validation strategy {@link Connection#isValid(int)} + */ + static ConnectionValidator defaultValidator() { + return new ConnectionValidator() { + @Override + public boolean isValid(Connection connection) { + try { + return connection.isValid(0); + } catch (Exception t) { + return false; + } + } + }; + } + + /** + * The default validation strategy with a timeout (in seconds). + * If the timeout period expires before the operation completes, the connection is invalidated. + */ + static ConnectionValidator defaultValidatorWithTimeout(int timeout) { + return new ConnectionValidator() { + @Override + public boolean isValid(Connection connection) { + try { + return connection.isValid(timeout); + } catch (Exception t) { + return false; + } + } + }; + } + + /** + * A validator that never invalidates connections. + */ + static ConnectionValidator emptyValidator() { + return new ConnectionValidator() { + @Override + public boolean isValid(Connection connection) { + return true; + } + }; + } + + // --- // + + /** + * @return true if a connection is valid, false otherwise + */ + boolean isValid(Connection connection); + } + + // --- // + + /** + * Interface for custom exception sorter strategies. Determines if a connection is still usable after an exception occurs. + */ + interface ExceptionSorter { + + /** + * Default exception sorter. Does not treat any exception as fatal. + */ + static ExceptionSorter defaultExceptionSorter() { + return new ExceptionSorter() { + @Override + public boolean isFatal(SQLException se) { + return false; + } + }; + } + + /** + * Never treat an exception as fatal. + */ + static ExceptionSorter emptyExceptionSorter() { + return new ExceptionSorter() { + @Override + public boolean isFatal(SQLException se) { + return false; + } + }; + } + + /** + * Treats every exception as fatal. + */ + static ExceptionSorter fatalExceptionSorter() { + return new ExceptionSorter() { + @Override + public boolean isFatal(SQLException se) { + return true; + } + }; + } + + /** + * @return true if an exception is considered fatal and the connection is not able for further use, false otherwise + */ + boolean isFatal(SQLException se); + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/XbibDataSourceConfiguration.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/XbibDataSourceConfiguration.java new file mode 100644 index 0000000..bfec9cf --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/XbibDataSourceConfiguration.java @@ -0,0 +1,62 @@ +package org.xbib.jdbc.pool.api.configuration; + +/** + * Configuration of an XbibDataSource. + */ +public interface XbibDataSourceConfiguration { + + /** + * Configuration of the pool of this DataSource + */ + XbibConnectionPoolConfiguration connectionPoolConfiguration(); + + /** + * Actual XbibDataSource implementation. This allows flexibility to have different implementations of the API. + */ + DataSourceImplementation dataSourceImplementation(); + + /** + * If this XbibDataSource collects metrics. + */ + boolean metricsEnabled(); + + /** + * Enables or disables the collection of metrics. + */ + void setMetricsEnabled(boolean metricsEnabled); + + /** + * Allows registration of a callback to be invoked each time there is a change on the status of metrics collection. + */ + void registerMetricsEnabledListener(MetricsEnabledListener metricsEnabledListener); + + /** + * Available implementations of XbibDataSource. + */ + enum DataSourceImplementation { + + /** + * Xbib - the database connection pool + */ + XBIB("org.xbib.jdbc.pool.DataSource"), + + /** + * Specialization of the Xbib pool for the flush-on-close use case. + */ + XBIB_POOLLESS("org.xbib.jdbc.pool.DataSource"); + + private final String className; + + DataSourceImplementation(String className) { + this.className = className; + } + + public String className() { + return className; + } + } + + interface MetricsEnabledListener { + void onMetricsEnabled(boolean metricsEnabled); + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibConnectionFactoryConfigurationSupplier.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibConnectionFactoryConfigurationSupplier.java new file mode 100644 index 0000000..cd20816 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibConnectionFactoryConfigurationSupplier.java @@ -0,0 +1,354 @@ +package org.xbib.jdbc.pool.api.configuration.supplier; + +import java.security.Principal; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Properties; +import java.util.function.Supplier; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionFactoryConfiguration; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionFactoryConfiguration.IsolationLevel; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionFactoryConfiguration.TransactionIsolation; +import org.xbib.jdbc.pool.api.security.XbibDefaultSecurityProvider; +import org.xbib.jdbc.pool.api.security.XbibKerberosSecurityProvider; +import org.xbib.jdbc.pool.api.security.XbibSecurityProvider; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionFactoryConfiguration.TransactionIsolation.UNDEFINED; + +/** + * Builder of XbibConnectionFactoryConfiguration. + */ +public class XbibConnectionFactoryConfigurationSupplier implements Supplier { + + private static final String USER_PROPERTY_NAME = "user"; + private static final String PASSWORD_PROPERTY_NAME = "password"; + + boolean autoCommit = true; + boolean trackJdbcResources = true; + Duration loginTimeout = Duration.ZERO; + String jdbcUrl = ""; + String initialSql = ""; + Class connectionProviderClass; + IsolationLevel transactionIsolation = UNDEFINED; + Collection securityProviders = new ArrayList<>(); + Principal principal; + Collection credentials = new ArrayList<>(); + Principal recoveryPrincipal; + Collection recoveryCredentials = new ArrayList<>(); + Properties jdbcProperties = new Properties(); + Properties xaProperties = new Properties(); + + private volatile boolean lock; + + public XbibConnectionFactoryConfigurationSupplier() { + lock = false; + securityProviders.add(new XbibDefaultSecurityProvider()); + securityProviders.add(new XbibKerberosSecurityProvider()); + } + + public XbibConnectionFactoryConfigurationSupplier(XbibConnectionFactoryConfiguration existingConfiguration) { + this(); + if (existingConfiguration == null) { + return; + } + autoCommit = existingConfiguration.autoCommit(); + loginTimeout = existingConfiguration.loginTimeout(); + jdbcUrl = existingConfiguration.jdbcUrl(); + initialSql = existingConfiguration.initialSql(); + connectionProviderClass = existingConfiguration.connectionProviderClass(); + transactionIsolation = existingConfiguration.jdbcTransactionIsolation(); + principal = existingConfiguration.principal(); + credentials = existingConfiguration.credentials(); + recoveryPrincipal = existingConfiguration.recoveryPrincipal(); + recoveryCredentials = existingConfiguration.recoveryCredentials(); + jdbcProperties = existingConfiguration.jdbcProperties(); + xaProperties = existingConfiguration.xaProperties(); + securityProviders = existingConfiguration.securityProviders(); + trackJdbcResources = existingConfiguration.trackJdbcResources(); + } + + private void checkLock() { + if (lock) { + throw new IllegalStateException("Attempt to modify an immutable configuration"); + } + } + + // --- // + + /** + * Sets the value of auto-commit for connections on the pool. Default is true. + */ + public XbibConnectionFactoryConfigurationSupplier autoCommit(boolean autoCommitEnabled) { + checkLock(); + autoCommit = autoCommitEnabled; + return this; + } + + /** + * Sets if JDBC resources are tracked to be closed if leaked. Default is true. + */ + public XbibConnectionFactoryConfigurationSupplier trackJdbcResources(boolean trackJdbcResourcesEnabled) { + checkLock(); + trackJdbcResources = trackJdbcResourcesEnabled; + return this; + } + + /** + * Sets the login timeout (in seconds). Default is 0 (waits indefinitely) + */ + public XbibConnectionFactoryConfigurationSupplier loginTimeout(Duration timeout) { + checkLock(); + loginTimeout = timeout; + return this; + } + + /** + * Sets the database URL to connect to. Default is "" + */ + public XbibConnectionFactoryConfigurationSupplier jdbcUrl(String jdbcUrlString) { + checkLock(); + jdbcUrl = jdbcUrlString; + return this; + } + + /** + * Sets the SQL command to be executed when a connection is created. + */ + public XbibConnectionFactoryConfigurationSupplier initialSql(String initialSqlString) { + checkLock(); + initialSql = initialSqlString; + return this; + } + + /** + * Sets a class from the JDBC driver to be used as a supplier of connections. + * Default is null, meaning the driver will be obtained from the URL (using the {@link java.sql.DriverManager#getDriver(String)} mechanism). + */ + public XbibConnectionFactoryConfigurationSupplier connectionProviderClass(Class connectionProvider) { + checkLock(); + connectionProviderClass = connectionProvider; + return this; + } + + /** + * Attempts to load a JDBC driver class using its fully qualified name. The classloader used is the one of this class. + * This method throws Exception if the class can't be loaded. + */ + public XbibConnectionFactoryConfigurationSupplier connectionProviderClassName(String connectionProviderName) { + try { + checkLock(); + connectionProviderClass = connectionProviderName == null ? null : Class.forName(connectionProviderName); + return this; + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Unable to load class " + connectionProviderName, e); + } + } + + /** + * Sets the transaction isolation level. Default is UNDEFINED, meaning that the default isolation level for the JDBC driver is used. + */ + public XbibConnectionFactoryConfigurationSupplier jdbcTransactionIsolation(TransactionIsolation transactionIsolationLevel) { + checkLock(); + transactionIsolation = transactionIsolationLevel; + return this; + } + + /** + * Allows setting a custom transaction isolation level. + */ + public XbibConnectionFactoryConfigurationSupplier jdbcTransactionIsolation(int customValue) { + checkLock(); + transactionIsolation = new CustomIsolationLevel(customValue); + return this; + } + + /** + * Allows setting additional {@link XbibSecurityProvider} to deal with custom principal/credential types. + * Default is to have {@link XbibDefaultSecurityProvider} and {@link XbibKerberosSecurityProvider} available by default. + */ + public XbibConnectionFactoryConfigurationSupplier addSecurityProvider(XbibSecurityProvider provider) { + checkLock(); + securityProviders.add(provider); + return this; + } + + /** + * Sets the principal to be authenticated in the database. Default is to don't perform authentication. + */ + public XbibConnectionFactoryConfigurationSupplier principal(Principal loginPrincipal) { + checkLock(); + principal = loginPrincipal; + return this; + } + + /** + * Sets credentials to use in order to authenticate to the database. Default is to don't provide any credentials. + */ + public XbibConnectionFactoryConfigurationSupplier credential(Object credential) { + checkLock(); + credentials.add(credential); + return this; + } + + /** + * Allows setting a different principal for recovery connections. + */ + public XbibConnectionFactoryConfigurationSupplier recoveryPrincipal(Principal loginPrincipal) { + checkLock(); + recoveryPrincipal = loginPrincipal; + return this; + } + + /** + * Allows providing a different set of credentials for recovery connections. + */ + public XbibConnectionFactoryConfigurationSupplier recoveryCredential(Object credential) { + checkLock(); + recoveryCredentials.add(credential); + return this; + } + + /** + * Allows setting other, unspecified, properties to be passed to the JDBC driver when creating new connections. + * NOTE: username and password properties are not allowed, these have to be set using the principal / credentials mechanism. + */ + public XbibConnectionFactoryConfigurationSupplier jdbcProperty(String key, String value) { + checkLock(); + jdbcProperties.setProperty(key, value); + return this; + } + + /** + * Allows setting other, unspecified, properties to be passed to the XA Datasource that override JDBC properties. + * NOTE: username and password properties are not allowed, these have to be set using the principal / credentials mechanism. + */ + public XbibConnectionFactoryConfigurationSupplier xaProperty(String key, String value) { + checkLock(); + xaProperties.setProperty(key, value); + return this; + } + + + // --- // + + @SuppressWarnings("stringConcatenation") + private void validate() { + if (lock) { + throw new IllegalStateException("Configuration already supplied"); + } + if (loginTimeout.isNegative()) { + throw new IllegalArgumentException("Login timeout must not be negative"); + } + if (jdbcProperties.containsKey(USER_PROPERTY_NAME)) { + throw new IllegalArgumentException("Invalid JDBC property '" + USER_PROPERTY_NAME + "': use principal instead."); + } + if (jdbcProperties.containsKey(PASSWORD_PROPERTY_NAME)) { + throw new IllegalArgumentException("Invalid property '" + PASSWORD_PROPERTY_NAME + "': use credential instead."); + } + } + + @Override + @SuppressWarnings("ReturnOfInnerClass") + public XbibConnectionFactoryConfiguration get() { + validate(); + lock = true; + + return new XbibConnectionFactoryConfiguration() { + + @Override + public boolean autoCommit() { + return autoCommit; + } + + @Override + public boolean trackJdbcResources() { + return trackJdbcResources; + } + + @Override + public Duration loginTimeout() { + return loginTimeout; + } + + @Override + public String jdbcUrl() { + return jdbcUrl; + } + + @Override + public String initialSql() { + return initialSql; + } + + @Override + public Class connectionProviderClass() { + return connectionProviderClass; + } + + @Override + public IsolationLevel jdbcTransactionIsolation() { + return transactionIsolation; + } + + @Override + public Collection securityProviders() { + return securityProviders; + } + + @Override + public Principal principal() { + return principal; + } + + @Override + public Collection credentials() { + return credentials; + } + + @Override + public Principal recoveryPrincipal() { + return recoveryPrincipal; + } + + @Override + public Collection recoveryCredentials() { + return recoveryCredentials; + } + + @Override + public Properties jdbcProperties() { + return jdbcProperties; + } + + @Override + public Properties xaProperties() { + return xaProperties; + } + }; + } + + // --- // + + private static class CustomIsolationLevel implements IsolationLevel { + + private final int customValue; + + public CustomIsolationLevel(int customValue) { + this.customValue = customValue; + } + + @Override + public boolean isDefined() { + return true; + } + + @Override + public int level() { + return customValue; + } + + @Override + public String toString() { + return "CUSTOM"; + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibConnectionPoolConfigurationSupplier.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibConnectionPoolConfigurationSupplier.java new file mode 100644 index 0000000..0fe8663 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibConnectionPoolConfigurationSupplier.java @@ -0,0 +1,473 @@ +package org.xbib.jdbc.pool.api.configuration.supplier; + +import java.time.Duration; +import java.util.function.Function; +import java.util.function.Supplier; +import org.xbib.jdbc.pool.api.cache.ConnectionCache; +import org.xbib.jdbc.pool.api.cache.LocalConnectionCache; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionFactoryConfiguration; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.MultipleAcquisitionAction; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.TransactionRequirement; +import org.xbib.jdbc.pool.api.transaction.TransactionIntegration; +import static java.lang.Integer.MAX_VALUE; +import static java.time.Duration.ZERO; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ConnectionValidator.emptyValidator; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter.emptyExceptionSorter; +import static org.xbib.jdbc.pool.api.transaction.TransactionIntegration.none; + +/** + * Builder of XbibConnectionPoolConfiguration. + */ +@SuppressWarnings({"PackageVisibleField", "WeakerAccess"}) +public class XbibConnectionPoolConfigurationSupplier implements Supplier { + + XbibConnectionFactoryConfiguration connectionFactoryConfiguration; + + ConnectionCache connectionCache = LocalConnectionCache.single(); + TransactionIntegration transactionIntegration = none(); + TransactionRequirement transactionRequirement = TransactionRequirement.OFF; + MultipleAcquisitionAction multipleAcquisitionAction = MultipleAcquisitionAction.OFF; + boolean enhancedLeakReport; + boolean flushOnClose; + int initialSize; + volatile int minSize; + volatile int maxSize = MAX_VALUE; + boolean validateOnBorrow; + XbibConnectionPoolConfiguration.ConnectionValidator connectionValidator = emptyValidator(); + XbibConnectionPoolConfiguration.ExceptionSorter exceptionSorter = emptyExceptionSorter(); + Duration idleValidationTimeout = ZERO; + Duration leakTimeout = ZERO; + Duration validationTimeout = ZERO; + Duration reapTimeout = ZERO; + Duration maxLifetime = ZERO; + volatile Duration acquisitionTimeout = ZERO; + + private volatile boolean lock; + + private XbibConnectionFactoryConfigurationSupplier connectionFactoryConfigurationSupplier = new XbibConnectionFactoryConfigurationSupplier(); + + public XbibConnectionPoolConfigurationSupplier() { + lock = false; + } + + public XbibConnectionPoolConfigurationSupplier(XbibConnectionPoolConfiguration existingConfiguration) { + this(); + if (existingConfiguration == null) { + return; + } + connectionFactoryConfigurationSupplier = new XbibConnectionFactoryConfigurationSupplier(existingConfiguration.connectionFactoryConfiguration()); + connectionCache = existingConfiguration.connectionCache(); + transactionIntegration = existingConfiguration.transactionIntegration(); + transactionRequirement = existingConfiguration.transactionRequirement(); + multipleAcquisitionAction = existingConfiguration.multipleAcquisition(); + flushOnClose = existingConfiguration.flushOnClose(); + enhancedLeakReport = existingConfiguration.enhancedLeakReport(); + initialSize = existingConfiguration.initialSize(); + minSize = existingConfiguration.minSize(); + maxSize = existingConfiguration.maxSize(); + validateOnBorrow = existingConfiguration.validateOnBorrow(); + connectionValidator = existingConfiguration.connectionValidator(); + exceptionSorter = existingConfiguration.exceptionSorter(); + idleValidationTimeout = existingConfiguration.idleValidationTimeout(); + leakTimeout = existingConfiguration.leakTimeout(); + validationTimeout = existingConfiguration.validationTimeout(); + reapTimeout = existingConfiguration.reapTimeout(); + maxLifetime = existingConfiguration.maxLifetime(); + acquisitionTimeout = existingConfiguration.acquisitionTimeout(); + } + + private void checkLock() { + if (lock) { + throw new IllegalStateException("Attempt to modify an immutable configuration"); + } + } + + /** + * Sets the configuration for the connection factory. + */ + private XbibConnectionPoolConfigurationSupplier connectionFactoryConfiguration(XbibConnectionFactoryConfiguration configuration) { + checkLock(); + connectionFactoryConfigurationSupplier = new XbibConnectionFactoryConfigurationSupplier(configuration); + return this; + } + + /** + * Sets the configuration for the connection factory. + */ + public XbibConnectionPoolConfigurationSupplier connectionFactoryConfiguration(Supplier supplier) { + return connectionFactoryConfiguration(supplier.get()); + } + + /** + * Modifies the configuration of the connection pool. + */ + public XbibConnectionPoolConfigurationSupplier connectionFactoryConfiguration(Function function) { + return connectionFactoryConfiguration(function.apply(connectionFactoryConfigurationSupplier)); + } + + /** + * Allows access to the configuration builder for the connection pool. + */ + public XbibConnectionFactoryConfigurationSupplier connectionFactoryConfiguration() { + return connectionFactoryConfigurationSupplier; + } + + // --- // + + /** + * Sets the connection cache implementation. Default is {@link LocalConnectionCache#single}. + */ + public XbibConnectionPoolConfigurationSupplier connectionCache(ConnectionCache cache) { + checkLock(); + connectionCache = cache; + return this; + } + + /** + * Sets the transaction integration instance to use. Default is {@link TransactionIntegration#none()}. + */ + public XbibConnectionPoolConfigurationSupplier transactionIntegration(TransactionIntegration integration) { + checkLock(); + transactionIntegration = integration; + return this; + } + + /** + * Sets the transaction requirements for the pool. Default is {@link TransactionRequirement#OFF}. + */ + public XbibConnectionPoolConfigurationSupplier transactionRequirement(TransactionRequirement requirement) { + checkLock(); + transactionRequirement = requirement; + return this; + } + + /** + * Enables enhanced leak report. + */ + public XbibConnectionPoolConfigurationSupplier enhancedLeakReport() { + return enhancedLeakReport(true); + } + + /** + * Sets the behaviour of the pool when a thread tries to acquire multiple connections. Default is {@link MultipleAcquisitionAction#OFF} + */ + public XbibConnectionPoolConfigurationSupplier multipleAcquisition(MultipleAcquisitionAction action) { + checkLock(); + multipleAcquisitionAction = action; + return this; + } + + /** + * Enables or disables enhanced leak report. Default is false. + */ + public XbibConnectionPoolConfigurationSupplier enhancedLeakReport(boolean enhanced) { + checkLock(); + enhancedLeakReport = enhanced; + return this; + } + + /** + * Enables flushing of connections on close. + */ + public XbibConnectionPoolConfigurationSupplier flushOnClose() { + return flushOnClose(true); + } + + /** + * Enables or disables flushing of connections on close. Default is false. + */ + public XbibConnectionPoolConfigurationSupplier flushOnClose(boolean flush) { + checkLock(); + flushOnClose = flush; + return this; + } + + /** + * Sets the number of connections when the pool starts. Must not be negative. Default is zero. + */ + public XbibConnectionPoolConfigurationSupplier initialSize(int size) { + checkLock(); + initialSize = size; + return this; + } + + /** + * Sets the minimum number of connections on the pool. Must not be negative and smaller than max. Default is zero. + */ + public XbibConnectionPoolConfigurationSupplier minSize(int size) { + checkLock(); + minSize = size; + return this; + } + + /** + * Sets the maximum number of connections on the pool. Must not be negative. Required. + */ + public XbibConnectionPoolConfigurationSupplier maxSize(int size) { + checkLock(); + maxSize = size; + return this; + } + + /** + * Enables validation on borrow. Default is false. + */ + public XbibConnectionPoolConfigurationSupplier validateOnBorrow(boolean validate) { + checkLock(); + validateOnBorrow = validate; + return this; + } + + /** + * Sets the connection validation method. Default {@link XbibConnectionPoolConfiguration.ConnectionValidator#emptyValidator()} + */ + public XbibConnectionPoolConfigurationSupplier connectionValidator(XbibConnectionPoolConfiguration.ConnectionValidator validator) { + checkLock(); + connectionValidator = validator; + return this; + } + + /** + * Sets the exception sorter. Default {@link XbibConnectionPoolConfiguration.ExceptionSorter#emptyExceptionSorter()} + */ + public XbibConnectionPoolConfigurationSupplier exceptionSorter(XbibConnectionPoolConfiguration.ExceptionSorter sorter) { + checkLock(); + exceptionSorter = sorter; + return this; + } + + /** + * Sets the duration of the acquisition timeout. Default is {@link Duration#ZERO} meaning that a thread will wait indefinitely. + */ + public XbibConnectionPoolConfigurationSupplier acquisitionTimeout(Duration timeout) { + checkLock(); + acquisitionTimeout = timeout; + return this; + } + + /** + * Sets the duration of idle time for foreground validation to be executed. Default is {@link Duration#ZERO} meaning that this feature is disabled. + */ + public XbibConnectionPoolConfigurationSupplier idleValidationTimeout(Duration timeout) { + checkLock(); + idleValidationTimeout = timeout; + return this; + } + + /** + * Sets the duration of the leak timeout detection. Default is {@link Duration#ZERO} meaning that this feature is disabled. + */ + public XbibConnectionPoolConfigurationSupplier leakTimeout(Duration timeout) { + checkLock(); + leakTimeout = timeout; + return this; + } + + /** + * Sets the duration of background validation interval. Default is {@link Duration#ZERO} meaning that this feature is disabled. + */ + public XbibConnectionPoolConfigurationSupplier validationTimeout(Duration timeout) { + checkLock(); + validationTimeout = timeout; + return this; + } + + /** + * Sets the duration for eviction of idle connections. Default is {@link Duration#ZERO} meaning that this feature is disabled. + */ + public XbibConnectionPoolConfigurationSupplier reapTimeout(Duration timeout) { + checkLock(); + reapTimeout = timeout; + return this; + } + + /** + * Sets the duration for the lifetime of connections. Default is {@link Duration#ZERO} meaning that this feature is disabled. + */ + public XbibConnectionPoolConfigurationSupplier maxLifetime(Duration time) { + checkLock(); + maxLifetime = time; + return this; + } + + // --- // + + private void validate() { + if (lock) { + throw new IllegalStateException("Configuration already supplied"); + } + if (maxSize == MAX_VALUE) { + throw new IllegalArgumentException("max size attribute is mandatory"); + } + if (maxSize <= 0) { + throw new IllegalArgumentException("A Positive max size is required"); + } + if (minSize < 0) { + throw new IllegalArgumentException("Invalid min size: smaller than 0"); + } + if (minSize > maxSize) { + throw new IllegalArgumentException("Invalid min size: greater than max size"); + } + if (initialSize < 0) { + throw new IllegalArgumentException("Invalid value for initial size. Must not be negative, and ideally between min size and max size"); + } + if (acquisitionTimeout.isNegative()) { + throw new IllegalArgumentException("Acquisition timeout must not be negative"); + } + if (idleValidationTimeout.isNegative()) { + throw new IllegalArgumentException("Idle validation timeout must not be negative"); + } + if (leakTimeout.isNegative()) { + throw new IllegalArgumentException("Leak detection timeout must not be negative"); + } + if (reapTimeout.isNegative()) { + throw new IllegalArgumentException("Reap timeout must not be negative"); + } + if (maxLifetime.isNegative()) { + throw new IllegalArgumentException("Max Lifetime must not be negative"); + } + if (validationTimeout.isNegative()) { + throw new IllegalArgumentException("Validation timeout must not be negative"); + } + if (connectionFactoryConfigurationSupplier == null) { + throw new IllegalArgumentException("Connection factory configuration not defined"); + } + connectionFactoryConfiguration = connectionFactoryConfigurationSupplier.get(); + } + + @Override + @SuppressWarnings("ReturnOfInnerClass") + public XbibConnectionPoolConfiguration get() { + validate(); + lock = true; + + return new XbibConnectionPoolConfiguration() { + + @Override + public XbibConnectionFactoryConfiguration connectionFactoryConfiguration() { + return connectionFactoryConfiguration; + } + + @Override + public ConnectionCache connectionCache() { + return connectionCache; + } + + @Override + public TransactionIntegration transactionIntegration() { + return transactionIntegration; + } + + @Override + public TransactionRequirement transactionRequirement() { + return transactionRequirement; + } + + @Override + public boolean enhancedLeakReport() { + return enhancedLeakReport; + } + + @Override + public boolean flushOnClose() { + return flushOnClose; + } + + @Override + public MultipleAcquisitionAction multipleAcquisition() { + return multipleAcquisitionAction; + } + + @Override + public int initialSize() { + return initialSize; + } + + @Override + public int minSize() { + return minSize; + } + + @Override + public void setMinSize(int size) { + if (size < 0) { + throw new IllegalArgumentException("Invalid min size: smaller than 0"); + } + if (size > maxSize) { + throw new IllegalArgumentException("Invalid min size: greater than max size"); + } + minSize = size; + } + + @Override + public int maxSize() { + return maxSize; + } + + @Override + public void setMaxSize(int size) { + if (size <= 0) { + throw new IllegalArgumentException("A Positive max size is required"); + } + if (size < minSize) { + throw new IllegalArgumentException("Invalid max size: smaller than min size"); + } + maxSize = size; + } + + @Override + public Duration acquisitionTimeout() { + return acquisitionTimeout; + } + + @Override + public void setAcquisitionTimeout(Duration timeout) { + if (timeout.isNegative()) { + throw new IllegalArgumentException("Acquisition timeout must not be negative"); + } + acquisitionTimeout = timeout; + } + + @Override + public boolean validateOnBorrow() { + return validateOnBorrow; + } + + @Override + public ConnectionValidator connectionValidator() { + return connectionValidator; + } + + @Override + public ExceptionSorter exceptionSorter() { + return exceptionSorter; + } + + @Override + public Duration idleValidationTimeout() { + return idleValidationTimeout; + } + + @Override + public Duration leakTimeout() { + return leakTimeout; + } + + @Override + public Duration validationTimeout() { + return validationTimeout; + } + + @Override + public Duration reapTimeout() { + return reapTimeout; + } + + @Override + public Duration maxLifetime() { + return maxLifetime; + } + }; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibDataSourceConfigurationSupplier.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibDataSourceConfigurationSupplier.java new file mode 100644 index 0000000..2010361 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibDataSourceConfigurationSupplier.java @@ -0,0 +1,139 @@ +package org.xbib.jdbc.pool.api.configuration.supplier; + +import java.util.function.Function; +import java.util.function.Supplier; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration; +import org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration; +import org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration.DataSourceImplementation; +import org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration.MetricsEnabledListener; + +/** + * Builder of XbibDataSourceConfiguration. + */ +public class XbibDataSourceConfigurationSupplier implements Supplier { + + XbibConnectionPoolConfiguration connectionPoolConfiguration; + + DataSourceImplementation dataSourceImplementation = DataSourceImplementation.XBIB; + volatile boolean metrics; + volatile MetricsEnabledListener metricsEnabledListener; + + private volatile boolean lock; + + private XbibConnectionPoolConfigurationSupplier connectionPoolConfigurationSupplier = new XbibConnectionPoolConfigurationSupplier(); + + public XbibDataSourceConfigurationSupplier() { + lock = false; + } + + private void checkLock() { + if (lock) { + throw new IllegalStateException("Attempt to modify an immutable configuration"); + } + } + + /** + * Sets the configuration of the connection pool. + */ + public XbibDataSourceConfigurationSupplier connectionPoolConfiguration(XbibConnectionPoolConfiguration configuration) { + checkLock(); + connectionPoolConfigurationSupplier = new XbibConnectionPoolConfigurationSupplier(configuration); + return this; + } + + /** + * Sets the configuration of the connection pool. + */ + public XbibDataSourceConfigurationSupplier connectionPoolConfiguration(Supplier supplier) { + return connectionPoolConfiguration(supplier.get()); + } + + /** + * Modifies the configuration of the connection pool. + */ + public XbibDataSourceConfigurationSupplier connectionPoolConfiguration(Function function) { + return connectionPoolConfiguration(function.apply(connectionPoolConfigurationSupplier)); + } + + /** + * Allows access to the configuration builder for the connection pool. + */ + public XbibConnectionPoolConfigurationSupplier connectionPoolConfiguration() { + return connectionPoolConfigurationSupplier; + } + + /** + * Selects the XbibDataSource implementation. + */ + public XbibDataSourceConfigurationSupplier dataSourceImplementation(DataSourceImplementation implementation) { + checkLock(); + dataSourceImplementation = implementation; + return this; + } + + /** + * Enables the collection of metrics. + */ + public XbibDataSourceConfigurationSupplier metricsEnabled() { + return metricsEnabled(true); + } + + /** + * Enables or disables the collection of metrics. The default is false. + */ + public XbibDataSourceConfigurationSupplier metricsEnabled(boolean metricsEnabled) { + checkLock(); + metrics = metricsEnabled; + return this; + } + + // --- // + + private void validate() { + if (lock) { + throw new IllegalStateException("Configuration already supplied"); + } + if (connectionPoolConfigurationSupplier == null) { + throw new IllegalArgumentException("Connection pool configuration not defined"); + } + connectionPoolConfiguration = connectionPoolConfigurationSupplier.get(); + } + + @Override + @SuppressWarnings("ReturnOfInnerClass") + public XbibDataSourceConfiguration get() { + validate(); + lock = true; + + return new XbibDataSourceConfiguration() { + + @Override + public XbibConnectionPoolConfiguration connectionPoolConfiguration() { + return connectionPoolConfiguration; + } + + @Override + public DataSourceImplementation dataSourceImplementation() { + return dataSourceImplementation; + } + + @Override + public boolean metricsEnabled() { + return metrics; + } + + @Override + public void setMetricsEnabled(boolean metricsEnabled) { + metrics = metricsEnabled; + if (metricsEnabledListener != null) { + metricsEnabledListener.onMetricsEnabled(metricsEnabled); + } + } + + @Override + public void registerMetricsEnabledListener(MetricsEnabledListener listener) { + metricsEnabledListener = listener; + } + }; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibPropertiesReader.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibPropertiesReader.java new file mode 100644 index 0000000..b695347 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/configuration/supplier/XbibPropertiesReader.java @@ -0,0 +1,336 @@ +package org.xbib.jdbc.pool.api.configuration.supplier; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.nio.file.Path; +import java.time.Duration; +import java.util.Map; +import java.util.Properties; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionFactoryConfiguration.TransactionIsolation; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ConnectionValidator; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.MultipleAcquisitionAction; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.TransactionRequirement; +import org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration; +import org.xbib.jdbc.pool.api.configuration.XbibDataSourceConfiguration.DataSourceImplementation; +import org.xbib.jdbc.pool.api.exceptionsorter.DB2ExceptionSorter; +import org.xbib.jdbc.pool.api.exceptionsorter.MSSQLExceptionSorter; +import org.xbib.jdbc.pool.api.exceptionsorter.MySQLExceptionSorter; +import org.xbib.jdbc.pool.api.exceptionsorter.OracleExceptionSorter; +import org.xbib.jdbc.pool.api.exceptionsorter.PostgreSQLExceptionSorter; +import org.xbib.jdbc.pool.api.exceptionsorter.SybaseExceptionSorter; +import org.xbib.jdbc.pool.api.security.NamePrincipal; +import org.xbib.jdbc.pool.api.security.SimplePassword; +import static java.lang.Long.parseLong; +import static java.util.function.Function.identity; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ConnectionValidator.defaultValidator; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ConnectionValidator.defaultValidatorWithTimeout; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ConnectionValidator.emptyValidator; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter.defaultExceptionSorter; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter.emptyExceptionSorter; +import static org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter.fatalExceptionSorter; + +/** + * Convenient way to build a configuration. + * This class can build a configuration from a *.properties file or a {@link Properties} object. + * This class defines keys for all the options and also allows for a prefix when looking for that properties. + */ +@SuppressWarnings("WeakerAccess") +public class XbibPropertiesReader implements Supplier { + + public static final String IMPLEMENTATION = "implementation"; + public static final String METRICS_ENABLED = "metricsEnabled"; + + public static final String MIN_SIZE = "minSize"; + public static final String MAX_SIZE = "maxSize"; + public static final String INITIAL_SIZE = "initialSize"; + public static final String FLUSH_ON_CLOSE = "flushOnClose"; + public static final String CONNECTION_VALIDATOR = "connectionValidator"; + public static final String ENHANCED_LEAK_REPORT = "enhancedLeakReport"; + public static final String EXCEPTION_SORTER = "exceptionSorter"; + public static final String MULTIPLE_ACQUISITION = "multipleAcquisition"; + public static final String TRANSACTION_REQUIREMENT = "transactionRequirement"; + public static final String VALIDATE_ON_BORROW = "validateOnBorrow"; + + public static final String ACQUISITION_TIMEOUT = "acquisitionTimeout"; + public static final String ACQUISITION_TIMEOUT_MS = "acquisitionTimeout_ms"; + public static final String ACQUISITION_TIMEOUT_S = "acquisitionTimeout_s"; + public static final String ACQUISITION_TIMEOUT_M = "acquisitionTimeout_m"; + + public static final String IDLE_VALIDATION = "idleValidation"; + public static final String IDLE_VALIDATION_MS = "idleValidation_ms"; + public static final String IDLE_VALIDATION_S = "idleValidation_s"; + public static final String IDLE_VALIDATION_M = "idleValidation_m"; + + public static final String VALIDATION_TIMEOUT = "validationTimeout"; + public static final String VALIDATION_TIMEOUT_MS = "validationTimeout_ms"; + public static final String VALIDATION_TIMEOUT_S = "validationTimeout_s"; + public static final String VALIDATION_TIMEOUT_M = "validationTimeout_m"; + + public static final String LEAK_TIMEOUT = "leakTimeout"; + public static final String LEAK_TIMEOUT_MS = "leakTimeout_ms"; + public static final String LEAK_TIMEOUT_S = "leakTimeout_s"; + public static final String LEAK_TIMEOUT_M = "leakTimeout_m"; + + public static final String REAP_TIMEOUT = "reapTimeout"; + public static final String REAP_TIMEOUT_MS = "reapTimeout_ms"; + public static final String REAP_TIMEOUT_S = "reapTimeout_s"; + public static final String REAP_TIMEOUT_M = "reapTimeout_m"; + + public static final String MAX_LIFETIME = "maxLifetime"; + public static final String MAX_LIFETIME_MS = "maxLifetime_ms"; + public static final String MAX_LIFETIME_S = "maxLifetime_s"; + public static final String MAX_LIFETIME_M = "maxLifetime_m"; + + // --- // + + public static final String JDBC_URL = "jdbcUrl"; + public static final String AUTO_COMMIT = "autoCommit"; + public static final String TRACK_JDBC_RESOURCES = "trackJdbcResources"; + public static final String LOGIN_TIMEOUT = "loginTimeout"; + public static final String INITIAL_SQL = "initialSQL"; + public static final String PROVIDER_CLASS_NAME = "providerClassName"; + public static final String TRANSACTION_ISOLATION = "jdbcTransactionIsolation"; + public static final String PRINCIPAL = "principal"; + public static final String CREDENTIAL = "credential"; + public static final String RECOVERY_PRINCIPAL = "recoveryPrincipal"; + public static final String RECOVERY_CREDENTIAL = "recoveryCredential"; + public static final String JDBC_PROPERTIES = "jdbcProperties"; + public static final String XA_PROPERTIES = "xaProperties"; + + // --- // + + private final String prefix; + + private final XbibDataSourceConfigurationSupplier dataSourceSupplier; + + public XbibPropertiesReader() { + this(""); + } + + public XbibPropertiesReader(String readerPrefix) { + prefix = readerPrefix; + dataSourceSupplier = new XbibDataSourceConfigurationSupplier(); + } + + /** + * Accepts the following options: + *
    + *
  • `empty` for the default {@link ConnectionValidator#emptyValidator()}
  • + *
  • `default` for {@link ConnectionValidator#defaultValidator()}
  • + *
  • `defaultX` for {@link ConnectionValidator#defaultValidatorWithTimeout(int)} where `X` is the timeout in seconds
  • + *
  • the name of a class that implements {@link ConnectionValidator}
  • + *
+ */ + public static ConnectionValidator parseConnectionValidator(String connectionValidatorName) { + if ("empty".equalsIgnoreCase(connectionValidatorName)) { + return emptyValidator(); + } else if ("default".equalsIgnoreCase(connectionValidatorName)) { + return defaultValidator(); + } else if (connectionValidatorName.regionMatches(true, 0, "default", 0, "default".length())) { + return defaultValidatorWithTimeout((int) parseDurationS(connectionValidatorName.substring("default".length())).toSeconds()); + } + + try { + Class validatorClass = Thread.currentThread().getContextClassLoader().loadClass(connectionValidatorName).asSubclass(ConnectionValidator.class); + return validatorClass.getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Unknown connection validator " + connectionValidatorName); + } catch (ClassCastException e) { + throw new IllegalArgumentException(connectionValidatorName + " class is not a ConnectionValidator", e); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException | + NoSuchMethodException e) { + throw new IllegalArgumentException("Unable to instantiate ConnectionValidator " + connectionValidatorName, e); + } + } + + /** + * Accepts the following options: + *
    + *
  • `empty` for the default {@link ExceptionSorter#emptyExceptionSorter()}
  • + *
  • `default` for {@link ExceptionSorter#defaultExceptionSorter()}
  • + *
  • `fatal` for {@link ExceptionSorter#fatalExceptionSorter()}
  • + *
  • `DB2` for the {@link DB2ExceptionSorter}
  • + *
  • `MSSQL` for the {@link MSSQLExceptionSorter}
  • + *
  • `MySQL` for the {@link MySQLExceptionSorter}
  • + *
  • `Oracle` for the {@link OracleExceptionSorter}
  • + *
  • `Postgres` or `PostgreSQL` for the {@link PostgreSQLExceptionSorter}
  • + *
  • `Sybase` for the {@link SybaseExceptionSorter}
  • + *
  • the name of a class that implements {@link ExceptionSorter}
  • + *
+ */ + public static ExceptionSorter parseExceptionSorter(String exceptionSorterName) { + if ("empty".equalsIgnoreCase(exceptionSorterName)) { + return emptyExceptionSorter(); + } else if ("default".equalsIgnoreCase(exceptionSorterName)) { + return defaultExceptionSorter(); + } else if ("fatal".equalsIgnoreCase(exceptionSorterName)) { + return fatalExceptionSorter(); + } else if ("DB2".equalsIgnoreCase(exceptionSorterName)) { + return new DB2ExceptionSorter(); + } else if ("MSSQL".equalsIgnoreCase(exceptionSorterName)) { + return new MSSQLExceptionSorter(); + } else if ("MySQL".equalsIgnoreCase(exceptionSorterName)) { + return new MySQLExceptionSorter(); + } else if ("Oracle".equalsIgnoreCase(exceptionSorterName)) { + return new OracleExceptionSorter(); + } else if ("Postgres".equalsIgnoreCase(exceptionSorterName) || "PostgreSQL".equalsIgnoreCase(exceptionSorterName)) { + return new PostgreSQLExceptionSorter(); + } else if ("Sybase".equalsIgnoreCase(exceptionSorterName)) { + return new SybaseExceptionSorter(); + } + try { + Class sorterClass = Thread.currentThread().getContextClassLoader().loadClass(exceptionSorterName).asSubclass(ExceptionSorter.class); + return sorterClass.getDeclaredConstructor().newInstance(); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("Unknown exception sorter " + exceptionSorterName); + } catch (ClassCastException e) { + throw new IllegalArgumentException(exceptionSorterName + " class is not a ExceptionSorter", e); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException | + NoSuchMethodException e) { + throw new IllegalArgumentException("Unable to instantiate ExceptionSorter " + exceptionSorterName, e); + } + } + + // --- // + + private static Duration parseDurationMs(String value) { + return Duration.ofMillis(parseLong(value)); + } + + private static Duration parseDurationS(String value) { + return Duration.ofSeconds(parseLong(value)); + } + + private static Duration parseDurationM(String value) { + return Duration.ofMinutes(parseLong(value)); + } + + @Override + public XbibDataSourceConfiguration get() { + return dataSourceSupplier.get(); + } + + public XbibDataSourceConfigurationSupplier modify() { + return dataSourceSupplier; + } + + public XbibPropertiesReader readProperties(Path path) throws IOException { + return readProperties(path.toFile()); + } + + public XbibPropertiesReader readProperties(String filename) throws IOException { + return readProperties(new File(filename)); + } + + // --- // + + public XbibPropertiesReader readProperties(File file) throws IOException { + try (InputStream inputStream = new FileInputStream(file)) { + Properties properties = new Properties(); + properties.load(inputStream); + return readProperties(properties); + } + } + + // --- // + + @SuppressWarnings("unchecked") + public XbibPropertiesReader readProperties(Properties properties) { + return readProperties((Map) properties); + } + + // --- // + + public XbibPropertiesReader readProperties(Map properties) { + XbibConnectionPoolConfigurationSupplier connectionPoolSupplier = new XbibConnectionPoolConfigurationSupplier(); + XbibConnectionFactoryConfigurationSupplier connectionFactorySupplier = new XbibConnectionFactoryConfigurationSupplier(); + + apply(dataSourceSupplier::dataSourceImplementation, DataSourceImplementation::valueOf, properties, IMPLEMENTATION); + apply(dataSourceSupplier::metricsEnabled, Boolean::parseBoolean, properties, METRICS_ENABLED); + + apply(connectionPoolSupplier::minSize, Integer::parseInt, properties, MIN_SIZE); + apply(connectionPoolSupplier::maxSize, Integer::parseInt, properties, MAX_SIZE); + apply(connectionPoolSupplier::flushOnClose, Boolean::parseBoolean, properties, FLUSH_ON_CLOSE); + apply(connectionPoolSupplier::initialSize, Integer::parseInt, properties, INITIAL_SIZE); + apply(connectionPoolSupplier::connectionValidator, XbibPropertiesReader::parseConnectionValidator, properties, CONNECTION_VALIDATOR); + apply(connectionPoolSupplier::exceptionSorter, XbibPropertiesReader::parseExceptionSorter, properties, EXCEPTION_SORTER); + apply(connectionPoolSupplier::enhancedLeakReport, Boolean::parseBoolean, properties, ENHANCED_LEAK_REPORT); + apply(connectionPoolSupplier::multipleAcquisition, MultipleAcquisitionAction::valueOf, properties, MULTIPLE_ACQUISITION); + apply(connectionPoolSupplier::transactionRequirement, TransactionRequirement::valueOf, properties, TRANSACTION_REQUIREMENT); + apply(connectionPoolSupplier::validateOnBorrow, Boolean::parseBoolean, properties, VALIDATE_ON_BORROW); + + apply(connectionPoolSupplier::acquisitionTimeout, Duration::parse, properties, ACQUISITION_TIMEOUT); + apply(connectionPoolSupplier::acquisitionTimeout, XbibPropertiesReader::parseDurationMs, properties, ACQUISITION_TIMEOUT_MS); + apply(connectionPoolSupplier::acquisitionTimeout, XbibPropertiesReader::parseDurationS, properties, ACQUISITION_TIMEOUT_S); + apply(connectionPoolSupplier::acquisitionTimeout, XbibPropertiesReader::parseDurationM, properties, ACQUISITION_TIMEOUT_M); + + apply(connectionPoolSupplier::idleValidationTimeout, Duration::parse, properties, IDLE_VALIDATION); + apply(connectionPoolSupplier::idleValidationTimeout, XbibPropertiesReader::parseDurationMs, properties, IDLE_VALIDATION_MS); + apply(connectionPoolSupplier::idleValidationTimeout, XbibPropertiesReader::parseDurationS, properties, IDLE_VALIDATION_S); + apply(connectionPoolSupplier::idleValidationTimeout, XbibPropertiesReader::parseDurationM, properties, IDLE_VALIDATION_M); + + apply(connectionPoolSupplier::validationTimeout, Duration::parse, properties, VALIDATION_TIMEOUT); + apply(connectionPoolSupplier::validationTimeout, XbibPropertiesReader::parseDurationMs, properties, VALIDATION_TIMEOUT_MS); + apply(connectionPoolSupplier::validationTimeout, XbibPropertiesReader::parseDurationS, properties, VALIDATION_TIMEOUT_S); + apply(connectionPoolSupplier::validationTimeout, XbibPropertiesReader::parseDurationM, properties, VALIDATION_TIMEOUT_M); + + apply(connectionPoolSupplier::leakTimeout, Duration::parse, properties, LEAK_TIMEOUT); + apply(connectionPoolSupplier::leakTimeout, XbibPropertiesReader::parseDurationMs, properties, LEAK_TIMEOUT_MS); + apply(connectionPoolSupplier::leakTimeout, XbibPropertiesReader::parseDurationS, properties, LEAK_TIMEOUT_S); + apply(connectionPoolSupplier::leakTimeout, XbibPropertiesReader::parseDurationM, properties, LEAK_TIMEOUT_M); + + apply(connectionPoolSupplier::reapTimeout, Duration::parse, properties, REAP_TIMEOUT); + apply(connectionPoolSupplier::reapTimeout, XbibPropertiesReader::parseDurationMs, properties, REAP_TIMEOUT_MS); + apply(connectionPoolSupplier::reapTimeout, XbibPropertiesReader::parseDurationS, properties, REAP_TIMEOUT_S); + apply(connectionPoolSupplier::reapTimeout, XbibPropertiesReader::parseDurationM, properties, REAP_TIMEOUT_M); + + apply(connectionPoolSupplier::maxLifetime, Duration::parse, properties, MAX_LIFETIME); + apply(connectionPoolSupplier::maxLifetime, XbibPropertiesReader::parseDurationMs, properties, MAX_LIFETIME_MS); + apply(connectionPoolSupplier::maxLifetime, XbibPropertiesReader::parseDurationS, properties, MAX_LIFETIME_S); + apply(connectionPoolSupplier::maxLifetime, XbibPropertiesReader::parseDurationM, properties, MAX_LIFETIME_M); + + apply(connectionFactorySupplier::jdbcUrl, identity(), properties, JDBC_URL); + apply(connectionFactorySupplier::autoCommit, Boolean::parseBoolean, properties, AUTO_COMMIT); + apply(connectionFactorySupplier::trackJdbcResources, Boolean::parseBoolean, properties, TRACK_JDBC_RESOURCES); + apply(connectionFactorySupplier::loginTimeout, Duration::parse, properties, LOGIN_TIMEOUT); + apply(connectionFactorySupplier::initialSql, identity(), properties, INITIAL_SQL); + apply(connectionFactorySupplier::connectionProviderClassName, identity(), properties, PROVIDER_CLASS_NAME); + apply(connectionFactorySupplier::jdbcTransactionIsolation, TransactionIsolation::valueOf, properties, TRANSACTION_ISOLATION); + apply(connectionFactorySupplier::principal, NamePrincipal::new, properties, PRINCIPAL); + apply(connectionFactorySupplier::credential, SimplePassword::new, properties, CREDENTIAL); + apply(connectionFactorySupplier::recoveryPrincipal, NamePrincipal::new, properties, RECOVERY_PRINCIPAL); + apply(connectionFactorySupplier::recoveryCredential, SimplePassword::new, properties, RECOVERY_CREDENTIAL); + applyJdbcProperties(connectionFactorySupplier::jdbcProperty, properties, JDBC_PROPERTIES); + applyJdbcProperties(connectionFactorySupplier::xaProperty, properties, XA_PROPERTIES); + + dataSourceSupplier.connectionPoolConfiguration(connectionPoolSupplier.connectionFactoryConfiguration(connectionFactorySupplier)); + return this; + } + + @SuppressWarnings("StringConcatenation") + private void apply(Consumer consumer, Function converter, Map properties, String key) { + String value = properties.get(prefix + key); + if (value != null) { + consumer.accept(converter.apply(value)); + } + } + + @SuppressWarnings({"SameParameterValue", "StringConcatenation"}) + private void applyJdbcProperties(BiConsumer consumer, Map properties, String key) { + String propertiesArray = properties.get(prefix + key); + if (propertiesArray != null && !propertiesArray.isEmpty()) { + for (String property : propertiesArray.split(";")) { + String[] keyValue = property.split("="); + consumer.accept(keyValue[0], keyValue[1]); + } + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/DB2ExceptionSorter.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/DB2ExceptionSorter.java new file mode 100644 index 0000000..312c9dd --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/DB2ExceptionSorter.java @@ -0,0 +1,20 @@ +package org.xbib.jdbc.pool.api.exceptionsorter; + +import java.sql.SQLException; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter; + +/** + * Exception sorter for DB2 databases. + */ +public class DB2ExceptionSorter implements ExceptionSorter { + + public DB2ExceptionSorter() { + } + + @Override + @SuppressWarnings("MagicNumber") + public boolean isFatal(SQLException e) { + int code = Math.abs(e.getErrorCode()); + return code == 4470 || code == 4499; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/MSSQLExceptionSorter.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/MSSQLExceptionSorter.java new file mode 100644 index 0000000..d80da61 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/MSSQLExceptionSorter.java @@ -0,0 +1,19 @@ +package org.xbib.jdbc.pool.api.exceptionsorter; + +import java.sql.SQLException; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter; + +/** + * Exception sorter for MSSQL databases. + */ +public class MSSQLExceptionSorter implements ExceptionSorter { + + public MSSQLExceptionSorter() { + } + + @Override + @SuppressWarnings("CallToSuspiciousStringMethod") + public boolean isFatal(SQLException e) { + return "08S01".equals(e.getSQLState()); + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/MySQLExceptionSorter.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/MySQLExceptionSorter.java new file mode 100644 index 0000000..bab9bea --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/MySQLExceptionSorter.java @@ -0,0 +1,26 @@ +package org.xbib.jdbc.pool.api.exceptionsorter; + +import java.sql.SQLException; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter; + +/** + * Exception sorter for MySQL / MariaDB databases. + */ +public class MySQLExceptionSorter implements ExceptionSorter { + + public MySQLExceptionSorter() { + } + + @Override + @SuppressWarnings("MagicNumber") + public boolean isFatal(SQLException e) { + if (e.getSQLState() != null && e.getSQLState().startsWith("08")) { + return true; + } + return switch (e.getErrorCode()) { + case 1040, 1042, 1043, 1047, 1081, 1129, 1130, 1045, 1004, 1005, 1015, 1021, 1041, 1037, 1038 -> true; + // All other errors + default -> false; + }; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/OracleExceptionSorter.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/OracleExceptionSorter.java new file mode 100644 index 0000000..7b23b8e --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/OracleExceptionSorter.java @@ -0,0 +1,51 @@ +package org.xbib.jdbc.pool.api.exceptionsorter; + +import java.sql.SQLException; +import java.util.Locale; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter; + +/** + * Exception sorter for Oracle databases. + */ +public class OracleExceptionSorter implements ExceptionSorter { + + public OracleExceptionSorter() { + } + + @Override + @SuppressWarnings({"CallToSuspiciousStringMethod", "MagicNumber"}) + public boolean isFatal(SQLException e) { + int errorCode = Math.abs(e.getErrorCode()); + + if (errorCode == 28 //session has been killed + || errorCode == 600 //Internal oracle error + || errorCode == 1012 //not logged on + || errorCode == 1014 //Oracle shutdown in progress + || errorCode == 1033 //Oracle initialization or shutdown in progress + || errorCode == 1034 //Oracle not available + || errorCode == 1035 //ORACLE only available to users with RESTRICTED SESSION privilege + || errorCode == 1089 //immediate shutdown in progress - no operations are permitted + || errorCode == 1090 //shutdown in progress - connection is not permitted + || errorCode == 1092 //ORACLE instance terminated. Disconnection forced + || errorCode == 1094 //ALTER DATABASE CLOSE in progress. Connections not permitted + || errorCode == 2396 //exceeded maximum idle time, please connect again + || errorCode == 3106 //fatal two-task communication protocol error + || errorCode == 3111 //break received on communication channel + || errorCode == 3113 //end-of-file on communication channel + || errorCode == 3114 //not connected to ORACLE + || errorCode >= 12100 && errorCode <= 12299 // TNS issues + || errorCode == 17002 //connection reset + || errorCode == 17008 //connection closed + || errorCode == 17410 //No more data to read from socket + || errorCode == 17447 //OALL8 is in an inconsistent state + ) { + return true; + } + // Exclude oracle user defined error codes (20000 through 20999) from consideration when looking for certain strings. + String errorText = (e.getMessage()).toUpperCase(Locale.US); + if ((errorCode < 20000 || errorCode >= 21000) && (errorText.contains("SOCKET") || errorText.contains("CONNECTION HAS ALREADY BEEN CLOSED") || errorText.contains("BROKEN PIPE"))) { + return true; + } + return "08000".equals(e.getSQLState()); + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/PostgreSQLExceptionSorter.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/PostgreSQLExceptionSorter.java new file mode 100644 index 0000000..6833a04 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/PostgreSQLExceptionSorter.java @@ -0,0 +1,19 @@ +package org.xbib.jdbc.pool.api.exceptionsorter; + +import java.sql.SQLException; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter; + +/** + * Exception sorter for PostgreSQL databases. + */ +public class PostgreSQLExceptionSorter implements ExceptionSorter { + + public PostgreSQLExceptionSorter() { + } + + @Override + public boolean isFatal(SQLException e) { + String sqlState = e.getSQLState(); + return sqlState != null && sqlState.startsWith("08"); // Class 08 — Connection Exception + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/SybaseExceptionSorter.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/SybaseExceptionSorter.java new file mode 100644 index 0000000..2aae027 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/exceptionsorter/SybaseExceptionSorter.java @@ -0,0 +1,22 @@ +package org.xbib.jdbc.pool.api.exceptionsorter; + +import java.sql.SQLException; +import java.util.Locale; +import org.xbib.jdbc.pool.api.configuration.XbibConnectionPoolConfiguration.ExceptionSorter; + +/** + * Exception sorter for Sybase databases. + */ +public class SybaseExceptionSorter implements ExceptionSorter { + + public SybaseExceptionSorter() { + } + + @Override + public boolean isFatal(SQLException e) { + String errorText = (e.getMessage()).toUpperCase(Locale.US); + + // ERR_CONNECTION_DEAD ERR_IOE_KILLED_CONNECTION CONNECTION CLOSED + return errorText.contains("JZ0C0") || errorText.contains("JZ0C1") || errorText.contains("JZ006"); + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/NamePrincipal.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/NamePrincipal.java new file mode 100644 index 0000000..e3d8f5b --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/NamePrincipal.java @@ -0,0 +1,51 @@ +package org.xbib.jdbc.pool.api.security; + +import java.security.Principal; +import java.util.Properties; + +/** + * A string that identifies an user account. + */ +public class NamePrincipal implements Principal { + + private final String name; + + public NamePrincipal(String name) { + this.name = name; + } + + public NamePrincipal(char[] name) { + this.name = new String(name); + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Principal p)) { + return false; + } + return name == null ? p.getName() == null : name.contentEquals(p.getName()); + } + + @Override + public int hashCode() { + return name == null ? 7 : name.hashCode(); + } + + @Override + public String toString() { + return name; + } + + public Properties asProperties() { + Properties properties = new Properties(); + properties.setProperty("user", name); + return properties; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/SimplePassword.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/SimplePassword.java new file mode 100644 index 0000000..6cb90d8 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/SimplePassword.java @@ -0,0 +1,51 @@ +package org.xbib.jdbc.pool.api.security; + +import java.util.Properties; + +/** + * Credential that holds a secret string of characters to be used for authentication. + */ +public class SimplePassword { + + private final String word; + + public SimplePassword(String password) { + word = password; + } + + public SimplePassword(char[] password) { + word = new String(password); + } + + public String getWord() { + return word; + } + + public Properties asProperties() { + Properties properties = new Properties(); + properties.setProperty("password", word); + return properties; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof SimplePassword that)) { + return false; + } + + return word == null ? that.word == null : word.contentEquals(that.word); + } + + @Override + public int hashCode() { + return word == null ? 7 : word.hashCode(); + } + + @Override + public String toString() { + return "*** masked ***"; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/XbibDefaultSecurityProvider.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/XbibDefaultSecurityProvider.java new file mode 100644 index 0000000..761385b --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/XbibDefaultSecurityProvider.java @@ -0,0 +1,30 @@ +package org.xbib.jdbc.pool.api.security; + +import java.security.Principal; +import java.util.Properties; + +/** + * Handles objects of type {@link NamePrincipal}, {@link SimplePassword} and {@link Principal} + */ +public class XbibDefaultSecurityProvider implements XbibSecurityProvider { + + public XbibDefaultSecurityProvider() { + } + + @Override + @SuppressWarnings("InstanceofConcreteClass") + public Properties getSecurityProperties(Object securityObject) { + if (securityObject instanceof NamePrincipal) { + return ((NamePrincipal) securityObject).asProperties(); + } + if (securityObject instanceof SimplePassword) { + return ((SimplePassword) securityObject).asProperties(); + } + if (securityObject instanceof Principal) { + Properties properties = new Properties(); + properties.setProperty("user", ((Principal) securityObject).getName()); + return properties; + } + return null; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/XbibKerberosSecurityProvider.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/XbibKerberosSecurityProvider.java new file mode 100644 index 0000000..c13f3c1 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/XbibKerberosSecurityProvider.java @@ -0,0 +1,38 @@ +package org.xbib.jdbc.pool.api.security; + +import java.util.Properties; +import javax.security.auth.kerberos.KerberosTicket; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.Oid; + +/** + * Handle objects of type {@link GSSCredential} and {@link KerberosTicket} + */ +public class XbibKerberosSecurityProvider implements XbibSecurityProvider { + + private static final Properties EMPTY_PROPERTIES = new Properties(); + + private static final String KERBEROS_v5 = "1.2.840.113554.1.2.2"; + + public XbibKerberosSecurityProvider() { + } + + @Override + public Properties getSecurityProperties(Object securityObject) { + if (securityObject instanceof GSSCredential) { + try { + Properties properties = new Properties(); + properties.setProperty("user", ((GSSCredential) securityObject).getName(new Oid(KERBEROS_v5)).toString()); + return properties; + } catch (GSSException e) { + // nothing we can do + return EMPTY_PROPERTIES; + } + } + if (securityObject instanceof KerberosTicket) { + return EMPTY_PROPERTIES; + } + return null; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/XbibSecurityProvider.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/XbibSecurityProvider.java new file mode 100644 index 0000000..98c3e32 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/security/XbibSecurityProvider.java @@ -0,0 +1,16 @@ +package org.xbib.jdbc.pool.api.security; + +import java.util.Properties; + +/** + * Interface to be implemented in order to extend with custom types of authentication. + */ +public interface XbibSecurityProvider { + + /** + * Converts a custom principal / credential objects to properties to be passed to the JDBC driver. + * + * @return null if not capable of handle the security object, otherwise return a {@link Properties} object even if empty. + */ + Properties getSecurityProperties(Object securityObject); +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/transaction/TransactionAware.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/transaction/TransactionAware.java new file mode 100644 index 0000000..85eb886 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/transaction/TransactionAware.java @@ -0,0 +1,64 @@ +package org.xbib.jdbc.pool.api.transaction; + +import java.sql.SQLException; + +/** + * Interface to be implemented by a resource (a connection) that the transaction integration layer will manipulate. + */ +public interface TransactionAware { + + /** + * The resource it's now enlisted with a transaction. + */ + void transactionStart() throws SQLException; + + /** + * The transaction is about to complete. + * + * @param successful If the transaction is to complete successfully (commit) or unsuccessfully (rollback) + */ + void transactionBeforeCompletion(boolean successful); + + /** + * The resource must commit. + */ + void transactionCommit() throws SQLException; + + /** + * The resource must rollback. + */ + void transactionRollback() throws SQLException; + + /** + * The transaction ended and the resource is no longer enlisted. + */ + void transactionEnd() throws SQLException; + + /** + * Set a callback trap to prevent lazy / deferred enlistment. We support neither of those features. + * This callback is set when the resource is obtained outside the scope of a running transaction and allows the resource to check if it's used within a transaction later on. + */ + void transactionCheckCallback(SQLCallable transactionCheck); + + /** + * Gets access to the raw {@link java.sql.Connection} held by the resource. + */ + Object getConnection(); + + /** + * The resource is no longer valid and should not be returned to the pool. + */ + void setFlushOnly(); + + /** + * A callable that can throw {@link SQLException} + */ + @FunctionalInterface + interface SQLCallable { + + /** + * Execute an action that may result in an {@link SQLException} + */ + T call() throws SQLException; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/transaction/TransactionIntegration.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/transaction/TransactionIntegration.java new file mode 100644 index 0000000..2dd527f --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/api/transaction/TransactionIntegration.java @@ -0,0 +1,92 @@ +package org.xbib.jdbc.pool.api.transaction; + +import java.sql.SQLException; +import javax.sql.XAConnection; +import javax.transaction.xa.XAResource; + +/** + * We provide an integration point for transaction systems to modify the behaviour of the pool. + * The transaction layer can control the lifecycle of connections and define what connections are acquired and when these return to the pool. + * It is responsible for commit or rollback of the database transaction. + */ +public interface TransactionIntegration { + + /** + * The default implementation of the transaction layer, that performs no actions. + */ + static TransactionIntegration none() { + return new TransactionIntegration() { + + @Override + public TransactionAware getTransactionAware() { + return null; + } + + @Override + public void associate(TransactionAware connection, XAResource xaResource) { + // nothing to do + } + + @Override + public boolean disassociate(TransactionAware connection) { + return true; + } + + @Override + public void addResourceRecoveryFactory(ResourceRecoveryFactory factory) { + // nothing to do + } + + @Override + public void removeResourceRecoveryFactory(ResourceRecoveryFactory factory) { + // nothing to do + } + }; + } + + /** + * We inquire the transaction layer for a Tx aware resource (a connection) that can be acquired. + * Usually, if there is a resource already associated with the calling thread it is returned. + */ + TransactionAware getTransactionAware() throws SQLException; + + /** + * We notify the transaction layer a Tx aware resource (a connection) and it's corresponding XA resource were acquired. + * Usually, the resource is associated with the calling thread. + */ + void associate(TransactionAware transactionAware, XAResource xaResource) throws SQLException; + + /** + * We notify the transaction layer that a Tx aware resource (a connection) is about to be returned to the pool. + * Usually, the resource is disassociated from the calling thread and returned to the pool. + * + * @return true if the Tx aware should return to the pool, false if it should not. + */ + boolean disassociate(TransactionAware transactionAware) throws SQLException; + + /** + * We call this method on init to register itself as a XA module capable of recovery. + */ + void addResourceRecoveryFactory(ResourceRecoveryFactory factory); + + /** + * We call this method on shutdown to de-register itself as a XA module capable of recovery. + */ + void removeResourceRecoveryFactory(ResourceRecoveryFactory factory); + + /** + * This interface is implemented by the connection factory so that it can provide recovery resources to the transaction layer. + */ + interface ResourceRecoveryFactory { + + /** + * The transaction layer can call this method to check if recovery is possible. + */ + boolean isRecoverable(); + + /** + * The transaction layer can call this method to obtain one connection used for recovery of incomplete transactions. + */ + XAConnection getRecoveryConnection() throws SQLException; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/AutoCloseableElement.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/AutoCloseableElement.java new file mode 100644 index 0000000..9577bfc --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/AutoCloseableElement.java @@ -0,0 +1,101 @@ +package org.xbib.jdbc.pool.util; + +import java.sql.SQLException; +import java.sql.Statement; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import static java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater; + +/** + * Abstract class to track auto-closeable resources by having them forming a collection between themselves. + * This class is designed to prevent leaks of JDBC wrapper objects. + * Once a new wrapper is created it tries to insert itself between the head and the first element of the list (if it exists). + * There is the invariant that at any given point in time the list can be traversed from the head and all inserted elements are reachable. + * As an implementation detail, the collection formed is actually a stack (FILO behaviour) and is thread-safe. + *

+ * The resources do not remove themselves on close. It's assumed that the implementations of this interface are wrappers and the contents are dropped at that moment. + * Also, allowing removal of elements would introduce an undesirable amount of complexity to this class. + */ +public abstract class AutoCloseableElement implements AutoCloseable { + + private static final AtomicReferenceFieldUpdater NEXT_UPDATER = newUpdater(AutoCloseableElement.class, AutoCloseableElement.class, "nextElement"); + + private volatile AutoCloseableElement nextElement; + + @SuppressWarnings("ThisEscapedInObjectConstruction") + protected AutoCloseableElement(AutoCloseableElement head) { + if (head != null) { + // point to the first element of the list and attempt to have the head to point at us + do { + nextElement = head.getNextElement(); + } while (!head.setNextElement(nextElement, this)); + } + } + + /** + * Create a special marker element to be used as head of a collection. + */ + public static AutoCloseableElement newHead() { + return new AutoCloseableElementHead(); + } + + public abstract void close() throws SQLException; + + public abstract boolean isClosed() throws Exception; + + /** + * Returns the number of resources that were not properly closed. The resources are closed in the process and the collection is cleared. + * This method should be invoked on the collection head only, otherwise it may not traverse the whole collection. + */ + public int closeAllAutocloseableElements() { + int count = 0; + // if under contention, the call that succeeds to reset the head is the one and only that will traverse the whole collection + for (AutoCloseableElement next = resetNextElement(); next != null; next = next.resetNextElement()) { + try { + if (!next.isClosed()) { + count++; + if (next instanceof Statement) { + try { + // AG-231 - we have to cancel the Statement on cleanup to avoid overloading the DB + ((Statement) next).cancel(); + } catch (SQLException e) { + // ignore and proceed with close() + } + } + next.close(); + } + } catch (Exception e) { + // ignore + } + } + return count; + } + + private boolean setNextElement(AutoCloseableElement expected, AutoCloseableElement element) { + return NEXT_UPDATER.compareAndSet(this, expected, element); + } + + private AutoCloseableElement getNextElement() { + return NEXT_UPDATER.get(this); + } + + private AutoCloseableElement resetNextElement() { + return NEXT_UPDATER.getAndSet(this, null); + } + + private static class AutoCloseableElementHead extends AutoCloseableElement { + + private AutoCloseableElementHead() { + super(null); + } + + @Override + public boolean isClosed() { + throw new IllegalStateException(); + } + + @Override + public void close() throws SQLException { + throw new IllegalStateException(); + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/InterceptorHelper.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/InterceptorHelper.java new file mode 100644 index 0000000..0961420 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/InterceptorHelper.java @@ -0,0 +1,54 @@ +package org.xbib.jdbc.pool.util; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import org.xbib.jdbc.pool.ConnectionHandler; +import org.xbib.jdbc.pool.api.XbibPoolInterceptor; + +@SuppressWarnings({"UtilityClass", "ObjectAllocationInLoop"}) +public final class InterceptorHelper { + + private InterceptorHelper() { + } + + public static void fireOnConnectionCreateInterceptor(List interceptors, ConnectionHandler handler) throws SQLException { + if (interceptors != null && !interceptors.isEmpty()) { + for (XbibPoolInterceptor interceptor : interceptors) { + try (Connection connection = handler.detachedWrapper()) { + interceptor.onConnectionCreate(connection); + } + } + } + } + + public static void fireOnConnectionAcquiredInterceptor(List interceptors, ConnectionHandler handler) throws SQLException { + if (interceptors != null && !interceptors.isEmpty()) { + for (XbibPoolInterceptor interceptor : interceptors) { + try (Connection connection = handler.detachedWrapper()) { + interceptor.onConnectionAcquire(connection); + } + } + } + } + + public static void fireOnConnectionReturnInterceptor(List interceptors, ConnectionHandler handler) throws SQLException { + if (interceptors != null && !interceptors.isEmpty()) { + for (int i = interceptors.size(); i > 0; ) { + try (Connection connection = handler.detachedWrapper()) { + interceptors.get(--i).onConnectionReturn(connection); + } + } + } + } + + public static void fireOnConnectionDestroyInterceptor(List interceptors, ConnectionHandler handler) throws SQLException { + if (interceptors != null && !interceptors.isEmpty()) { + for (int i = interceptors.size(); i > 0; ) { + try (Connection connection = handler.detachedWrapper()) { + interceptors.get(--i).onConnectionDestroy(connection); + } + } + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/ListenerHelper.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/ListenerHelper.java new file mode 100644 index 0000000..061e0be --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/ListenerHelper.java @@ -0,0 +1,158 @@ +package org.xbib.jdbc.pool.util; + +import java.sql.Connection; +import java.util.Arrays; +import org.xbib.jdbc.pool.ConnectionHandler; +import org.xbib.jdbc.pool.api.XbibDataSourceListener; + +@SuppressWarnings("UtilityClass") +public final class ListenerHelper { + + private ListenerHelper() { + } + + public static void fireBeforeConnectionCreation(XbibDataSourceListener[] listeners) { + for (XbibDataSourceListener listener : listeners) { + listener.beforeConnectionCreation(); + } + } + + public static void fireOnConnectionCreation(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.onConnectionCreation(handler.rawConnection()); + } + } + + public static void fireOnConnectionPooled(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.onConnectionPooled(handler.rawConnection()); + } + } + + public static void fireBeforeConnectionAcquire(XbibDataSourceListener[] listeners) { + for (XbibDataSourceListener listener : listeners) { + listener.beforeConnectionAcquire(); + } + } + + public static void fireOnConnectionAcquired(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.onConnectionAcquire(handler.rawConnection()); + } + } + + public static void fireBeforeConnectionReturn(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.beforeConnectionReturn(handler.rawConnection()); + } + } + + public static void fireOnConnectionReturn(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.onConnectionReturn(handler.rawConnection()); + } + } + + public static void fireBeforeConnectionLeak(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.beforeConnectionLeak(handler.rawConnection()); + } + } + + @SuppressWarnings({"StringConcatenation", "ObjectAllocationInLoop"}) + public static void fireOnConnectionLeak(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + Connection connection = handler.rawConnection(); + listener.onConnectionLeak(connection, handler.getHoldingThread()); + if (handler.getAcquisitionStackTrace() != null) { + if (handler.isEnlisted()) { + listener.onInfo("Leaked connection " + connection + " is enlisted. Please make sure the associated transaction completes."); + } else { + listener.onInfo("Leaked connection " + connection + " is not enlisted. To return it to the pool use the flush(LEAK) operation."); + } + listener.onInfo("Leaked connection " + connection + " acquired at: " + Arrays.toString(handler.getAcquisitionStackTrace())); + } + if (handler.getConnectionOperations() != null) { + listener.onInfo("Operations executed on leaked connection " + connection + ": " + String.join(", ", handler.getConnectionOperations())); + } + if (handler.getLastOperationStackTrace() != null) { + listener.onInfo("Stack trace of last executed operation on " + connection + ": " + Arrays.toString(handler.getLastOperationStackTrace())); + } + if (handler.getConnectionOperations() != null && handler.getConnectionOperations().contains("unwrap(Class)")) { + listener.onWarning("A possible cause for the leak of connection " + connection + " is a call to the unwrap() method. close() needs to be called on the connection object provided by the pool."); + } + } + } + + public static void fireBeforeConnectionValidation(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.beforeConnectionValidation(handler.rawConnection()); + } + } + + public static void fireOnConnectionValid(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.onConnectionValid(handler.rawConnection()); + } + } + + public static void fireOnConnectionInvalid(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.onConnectionInvalid(handler.rawConnection()); + } + } + + public static void fireBeforeConnectionFlush(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.beforeConnectionFlush(handler.rawConnection()); + } + } + + public static void fireOnConnectionFlush(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.onConnectionFlush(handler.rawConnection()); + } + } + + public static void fireBeforeConnectionReap(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.beforeConnectionReap(handler.rawConnection()); + } + } + + public static void fireOnConnectionReap(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.onConnectionReap(handler.rawConnection()); + } + } + + public static void fireBeforeConnectionDestroy(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.beforeConnectionDestroy(handler.rawConnection()); + } + } + + public static void fireOnConnectionDestroy(XbibDataSourceListener[] listeners, ConnectionHandler handler) { + for (XbibDataSourceListener listener : listeners) { + listener.onConnectionDestroy(handler.rawConnection()); + } + } + + public static void fireOnWarning(XbibDataSourceListener[] listeners, String message) { + for (XbibDataSourceListener listener : listeners) { + listener.onWarning(message); + } + } + + public static void fireOnWarning(XbibDataSourceListener[] listeners, Throwable throwable) { + for (XbibDataSourceListener listener : listeners) { + listener.onWarning(throwable); + } + } + + public static void fireOnInfo(XbibDataSourceListener[] listeners, String message) { + for (XbibDataSourceListener listener : listeners) { + listener.onInfo(message); + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/PriorityScheduledExecutor.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/PriorityScheduledExecutor.java new file mode 100644 index 0000000..11dcbed --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/PriorityScheduledExecutor.java @@ -0,0 +1,119 @@ +package org.xbib.jdbc.pool.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.RunnableFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicLong; +import org.xbib.jdbc.pool.api.XbibDataSourceListener; +import static org.xbib.jdbc.pool.util.ListenerHelper.fireOnWarning; + +public final class PriorityScheduledExecutor extends ScheduledThreadPoolExecutor { + + private static final Runnable EMPTY_TASK = () -> { + }; + + private final Queue> priorityTasks = new ConcurrentLinkedQueue<>(); + private final XbibDataSourceListener[] listeners; + + public PriorityScheduledExecutor(int executorSize, String threadPrefix, XbibDataSourceListener... listeners) { + super(executorSize, new PriorityExecutorThreadFactory(threadPrefix), new CallerRunsPolicy()); + setRemoveOnCancelPolicy(true); + this.listeners = listeners; + } + + @SuppressWarnings("WeakerAccess") + public void executeNow(Runnable priorityTask) { + executeNow(new FutureTask<>(priorityTask, null)); + } + + @SuppressWarnings("WeakerAccess") + public Future executeNow(Callable priorityTask) { + return executeNow(new FutureTask<>(priorityTask)); + } + + @SuppressWarnings("WeakerAccess") + public Future executeNow(RunnableFuture priorityFuture) { + if (isShutdown()) { + throw new RejectedExecutionException("Task " + priorityFuture + " rejected from " + this); + } + priorityTasks.add(priorityFuture); + if (!priorityFuture.isDone()) { + // Submit a task so that the beforeExecute() method gets called + execute(EMPTY_TASK); + } + return priorityFuture; + } + + @Override + protected void beforeExecute(Thread thread, Runnable lowPriorityTask) { + // Run all high priority tasks in queue first, then low priority + for (RunnableFuture priorityTask; (priorityTask = priorityTasks.poll()) != null; ) { + if (isShutdown()) { + priorityTask.cancel(false); + } else { + try { + priorityTask.run(); + afterExecute(priorityTask, null); + } catch (Throwable t) { + afterExecute(priorityTask, t); + } + } + } + super.beforeExecute(thread, lowPriorityTask); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + if (t != null) { + fireOnWarning(listeners, t); + } + super.afterExecute(r, t); + } + + @Override + public void shutdown() { + if (!isShutdown()) { + executeNow(super::shutdown); + execute(this::shutdownNow); + } + } + + @Override + public List shutdownNow() { + for (RunnableFuture runnableFuture : priorityTasks) { + runnableFuture.cancel(true); + } + List allTasks = new ArrayList<>(priorityTasks); + priorityTasks.clear(); + + allTasks.addAll(super.shutdownNow()); + return allTasks; + } + + private static class PriorityExecutorThreadFactory implements ThreadFactory { + + private final AtomicLong threadCount; + private final String threadPrefix; + + @SuppressWarnings("WeakerAccess") + PriorityExecutorThreadFactory(String threadPrefix) { + this.threadPrefix = threadPrefix; + threadCount = new AtomicLong(); + } + + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, threadPrefix + threadCount.incrementAndGet()); + thread.setDaemon(true); + return thread; + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/PropertyInjector.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/PropertyInjector.java new file mode 100644 index 0000000..4441f9f --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/PropertyInjector.java @@ -0,0 +1,110 @@ +package org.xbib.jdbc.pool.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +public final class PropertyInjector { + + private final Class cls; + + private final Map propertySetter = new HashMap<>(); + + @SuppressWarnings("CallToSuspiciousStringMethod") + public PropertyInjector(Class cls) { + this.cls = cls; + + for (Method method : cls.getMethods()) { + String name = method.getName(); + if (method.getParameterCount() == 1 && name.startsWith("set")) { + propertySetter.merge(name.substring(3), method, PropertyInjector::methodSelector); + } else if (method.getParameterCount() == 2 && "setProperty".equals(name)) { + propertySetter.put("Property", method); + } + } + } + + /** + * Method to resolve ambiguities when multiple methods apply the same property. + */ + private static Method methodSelector(Method methodOne, Method methodTwo) { + // AG-200 - Prefer methods that are not deprecated + return methodOne.isAnnotationPresent(Deprecated.class) ? methodTwo : methodOne; + } + + @SuppressWarnings("CallToSuspiciousStringMethod") + private static Properties typeConvertProperties(String properties) { + if (properties == null) { + return new Properties(); + } + properties = properties.trim(); + if (properties.isEmpty()) { + return new Properties(); + } + Properties result = new Properties(); + for (String property : properties.split(";")) { + String[] keyValue = property.split("="); + if (keyValue.length != 2) { + throw new IllegalArgumentException("Can't convert properties '" + properties + "' to " + Properties.class.getName()); + } + result.put(keyValue[0].trim(), keyValue[1].trim()); + } + return result; + } + + @SuppressWarnings("StringConcatenation") + public void inject(Object target, String propertyName, String propertyValue) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + String realName = propertyName.substring(0, 1).toUpperCase(Locale.ROOT) + propertyName.substring(1); + if (propertySetter.containsKey(realName)) { + Method method = propertySetter.get(realName); + method.invoke(target, typeConvert(propertyValue, method.getParameterTypes()[0])); + } else if (propertySetter.containsKey("Property")) { + cls.getMethod("setProperty", propertyName.getClass(), propertyValue.getClass()).invoke(target, propertyName, propertyValue); + } else { + throw new NoSuchMethodException("No setter in class " + cls.getName()); + } + } + + public Set availableProperties() { + return propertySetter.keySet(); + } + + private Object typeConvert(String value, Class target) { + if (target == String.class) { + return value; + } else if (target == byte[].class) { + return value.getBytes(StandardCharsets.UTF_8); + } else if (target == char[].class) { + return value.toCharArray(); + } else if (Boolean.class.isAssignableFrom(target) || Boolean.TYPE.isAssignableFrom(target)) { + return Boolean.parseBoolean(value); + } else if (Character.class.isAssignableFrom(target) || Character.TYPE.isAssignableFrom(target)) { + return value.charAt(0); + } else if (Short.class.isAssignableFrom(target) || Short.TYPE.isAssignableFrom(target)) { + return Short.parseShort(value); + } else if (Integer.class.isAssignableFrom(target) || Integer.TYPE.isAssignableFrom(target)) { + return Integer.parseInt(value); + } else if (Long.class.isAssignableFrom(target) || Long.TYPE.isAssignableFrom(target)) { + return Long.parseLong(value); + } else if (Float.class.isAssignableFrom(target) || Float.TYPE.isAssignableFrom(target)) { + return Float.parseFloat(value); + } else if (Double.class.isAssignableFrom(target) || Double.TYPE.isAssignableFrom(target)) { + return Double.parseDouble(value); + } else if (Class.class.isAssignableFrom(target)) { + try { + return cls.getClassLoader().loadClass(value); + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException("ClassNotFoundException: " + e.getMessage()); + } + } else if (Properties.class.isAssignableFrom(target)) { + return typeConvertProperties(value); + } else { + throw new IllegalArgumentException("Can't convert to " + target.getName()); + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/StampedCopyOnWriteArrayList.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/StampedCopyOnWriteArrayList.java new file mode 100644 index 0000000..dff8be0 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/StampedCopyOnWriteArrayList.java @@ -0,0 +1,330 @@ +package org.xbib.jdbc.pool.util; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.concurrent.locks.StampedLock; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import static java.lang.System.arraycopy; +import static java.lang.reflect.Array.newInstance; + +public final class StampedCopyOnWriteArrayList implements List { + + private final Iterator emptyIterator = new Iterator() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public T next() { + throw new NoSuchElementException(); + } + }; + + private final StampedLock lock; + private long optimisticStamp; + private T[] data; + + @SuppressWarnings("unchecked") + public StampedCopyOnWriteArrayList(Class clazz) { + data = (T[]) newInstance(clazz, 0); + lock = new StampedLock(); + optimisticStamp = lock.tryOptimisticRead(); + } + + private T[] getUnderlyingArray() { + T[] array = data; + if (lock.validate(optimisticStamp)) { + return array; + } + + // Acquiring a read lock does not increment the optimistic stamp + long stamp = lock.readLock(); + try { + return data; + } finally { + lock.unlockRead(stamp); + } + } + + @Override + public T get(int index) { + return getUnderlyingArray()[index]; + } + + @Override + public int size() { + return getUnderlyingArray().length; + } + + // --- // + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public T set(int index, T element) { + long stamp = lock.writeLock(); + try { + T old = data[index]; + data[index] = element; + return old; + } finally { + optimisticStamp = lock.tryConvertToOptimisticRead(stamp); + } + } + + @Override + public boolean add(T element) { + long stamp = lock.writeLock(); + try { + data = Arrays.copyOf(data, data.length + 1); + data[data.length - 1] = element; + return true; + } finally { + optimisticStamp = lock.tryConvertToOptimisticRead(stamp); + } + } + + public T removeLast() { + long stamp = lock.writeLock(); + try { + T element = data[data.length - 1]; + data = Arrays.copyOf(data, data.length - 1); + return element; + } finally { + optimisticStamp = lock.tryConvertToOptimisticRead(stamp); + } + } + + @Override + public boolean remove(Object element) { + int index = indexOf(element); + if (index == -1) { + return false; + } + long stamp = lock.writeLock(); + try { + if (index >= data.length || element != data[index]) { + // contents changed, need to recheck the position of the element in the array + int length = data.length; + for (index = 0; index < length; index++) { + if (element == data[index]) { + break; + } + } + if (index == data.length) { // not found! + return false; + } + } + T[] newData = Arrays.copyOf(data, data.length - 1); + if (data.length - index - 1 != 0) { + arraycopy(data, index + 1, newData, index, data.length - index - 1); + } + data = newData; + return true; + } finally { + optimisticStamp = lock.tryConvertToOptimisticRead(stamp); + } + } + + @Override + public T remove(int index) { + long stamp = lock.writeLock(); + try { + T old = data[index]; + T[] array = Arrays.copyOf(data, data.length - 1); + if (data.length - index - 1 != 0) { + arraycopy(data, index + 1, array, index, data.length - index - 1); + } + data = array; + return old; + } finally { + optimisticStamp = lock.tryConvertToOptimisticRead(stamp); + } + } + + @Override + public void clear() { + long stamp = lock.writeLock(); + try { + data = Arrays.copyOf(data, 0); + } finally { + optimisticStamp = lock.tryConvertToOptimisticRead(stamp); + } + } + + @Override + public Iterator iterator() { + T[] array = getUnderlyingArray(); + return array.length == 0 ? emptyIterator : new UncheckedIterator<>(array); + } + + @Override + public boolean addAll(Collection c) { + long stamp = lock.writeLock(); + try { + int oldSize = data.length; + data = Arrays.copyOf(data, oldSize + c.size()); + for (T element : c) { + data[oldSize++] = element; + } + return true; + } finally { + optimisticStamp = lock.tryConvertToOptimisticRead(stamp); + } + } + + @Override + public boolean contains(Object o) { + return indexOf(o) != -1; + } + + @Override + public int indexOf(Object o) { + T[] array = getUnderlyingArray(); + int length = array.length; + for (int i = 0; i < length; i++) { + if (o == array[i]) { + return i; + } + } + return -1; + } + + @Override + public void forEach(Consumer action) { + for (T element : this) { + action.accept(element); + } + } + + // --- // + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public E[] toArray(E[] a) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(int index, T element) { + throw new UnsupportedOperationException(); + } + + @Override + public int lastIndexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public Stream stream() { + throw new UnsupportedOperationException(); + } + + @Override + public Stream parallelStream() { + throw new UnsupportedOperationException(); + } + + @Override + public Spliterator spliterator() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeIf(Predicate filter) { + throw new UnsupportedOperationException(); + } + + @Override + public void replaceAll(UnaryOperator operator) { + throw new UnsupportedOperationException(); + } + + @Override + public void sort(Comparator c) { + throw new UnsupportedOperationException(); + } + + // --- // + + private static final class UncheckedIterator implements Iterator { + + private final int size; + + private final T[] data; + + private int index; + + @SuppressWarnings("WeakerAccess") + UncheckedIterator(T[] array) { + data = array; + size = data.length; + } + + @Override + public boolean hasNext() { + return index < size; + } + + @Override + public T next() { + if (index < size) { + return data[index++]; + } + throw new NoSuchElementException("No more elements in this list"); + } + } + +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/UncheckedArrayList.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/UncheckedArrayList.java new file mode 100644 index 0000000..1d50507 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/UncheckedArrayList.java @@ -0,0 +1,261 @@ +package org.xbib.jdbc.pool.util; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import static java.lang.System.arraycopy; +import static java.lang.reflect.Array.newInstance; + +public final class UncheckedArrayList implements List { + + private final Iterator emptyIterator = new Iterator() { + @Override + public boolean hasNext() { + return false; + } + + @Override + public T next() { + throw new NoSuchElementException(); + } + }; + + private T[] data; + private int size; + + @SuppressWarnings("unchecked") + private UncheckedArrayList(Class clazz, int capacity) { + data = (T[]) newInstance(clazz, capacity); + size = 0; + } + + public UncheckedArrayList(Class clazz) { + this(clazz, 4); + } + + public UncheckedArrayList(Class clazz, T[] initial) { + this(clazz, initial.length); + arraycopy(initial, 0, data, 0, size); + } + + @Override + public boolean add(T element) { + if (size >= data.length) { + data = Arrays.copyOf(data, data.length << 1); + } + data[size] = element; + size++; + return true; + } + + @Override + public T get(int index) { + return data[index]; + } + + @Override + public boolean remove(Object element) { + for (int index = size - 1; index >= 0; index--) { + if (element == data[index]) { + int numMoved = size - index - 1; + if (numMoved > 0) { + arraycopy(data, index + 1, data, index, numMoved); + } + data[--size] = null; + return true; + } + } + return false; + } + + @Override + public void clear() { + for (int i = 0; i < size; i++) { + data[i] = null; + } + size = 0; + } + + @Override + public int size() { + return size; + } + + @Override + public boolean isEmpty() { + return size == 0; + } + + @Override + public T set(int index, T element) { + T old = data[index]; + data[index] = element; + return old; + } + + @Override + public T remove(int index) { + if (size == 0) { + return null; + } + T old = data[index]; + int moved = size - index - 1; + if (moved > 0) { + arraycopy(data, index + 1, data, index, moved); + } + data[--size] = null; + return old; + } + + @Override + public boolean contains(Object o) { + for (T element : this) { + if (element == o) { + return true; + } + } + return false; + } + + @Override + public Iterator iterator() { + return size == 0 ? emptyIterator : new UncheckedIterator<>(data, size); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public E[] toArray(E[] a) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean containsAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(int index, Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection c) { + throw new UnsupportedOperationException(); + } + + @Override + public void add(int index, T element) { + throw new UnsupportedOperationException(); + } + + @Override + public int indexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public int lastIndexOf(Object o) { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator() { + throw new UnsupportedOperationException(); + } + + @Override + public ListIterator listIterator(int index) { + throw new UnsupportedOperationException(); + } + + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + @Override + public Stream stream() { + throw new UnsupportedOperationException(); + } + + @Override + public Stream parallelStream() { + throw new UnsupportedOperationException(); + } + + @Override + public void forEach(Consumer action) { + throw new UnsupportedOperationException(); + } + + @Override + public Spliterator spliterator() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeIf(Predicate filter) { + throw new UnsupportedOperationException(); + } + + @Override + public void replaceAll(UnaryOperator operator) { + throw new UnsupportedOperationException(); + } + + @Override + public void sort(Comparator c) { + throw new UnsupportedOperationException(); + } + + private static final class UncheckedIterator implements Iterator { + + private final int size; + + private final T[] data; + + private int index; + + @SuppressWarnings("WeakerAccess") + UncheckedIterator(T[] data, int size) { + this.data = data; + this.size = size; + } + + @Override + public boolean hasNext() { + return index < size; + } + + @Override + public T next() { + if (index < size) { + return data[index++]; + } + throw new NoSuchElementException("No more elements in this list"); + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/XAConnectionAdaptor.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/XAConnectionAdaptor.java new file mode 100644 index 0000000..4d9b096 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/XAConnectionAdaptor.java @@ -0,0 +1,55 @@ +package org.xbib.jdbc.pool.util; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.ConnectionEventListener; +import javax.sql.StatementEventListener; +import javax.sql.XAConnection; +import javax.transaction.xa.XAResource; + +/** + * Disguises a non-XA connection as an XAConnection. Useful to keep the same logic for pooling both XA and non-XA connections + */ +public class XAConnectionAdaptor implements XAConnection { + + private final Connection connection; + + public XAConnectionAdaptor(Connection connection) { + this.connection = connection; + } + + @Override + public Connection getConnection() { + return connection; + } + + @Override + public void close() throws SQLException { + connection.close(); + } + + @Override + public void addConnectionEventListener(ConnectionEventListener listener) { + throw new IllegalArgumentException("no ConnectionEventListener on non-XA connection"); + } + + @Override + public void removeConnectionEventListener(ConnectionEventListener listener) { + throw new IllegalArgumentException("no ConnectionEventListener on non-XA connection"); + } + + @Override + public void addStatementEventListener(StatementEventListener listener) { + throw new IllegalArgumentException("no StatementEventListener on non-XA connection"); + } + + @Override + public void removeStatementEventListener(StatementEventListener listener) { + throw new IllegalArgumentException("no StatementEventListener on non-XA connection"); + } + + @Override + public XAResource getXAResource() throws SQLException { + return null; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/XbibSynchronizer.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/XbibSynchronizer.java new file mode 100644 index 0000000..b5a4d92 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/util/XbibSynchronizer.java @@ -0,0 +1,31 @@ +package org.xbib.jdbc.pool.util; + +import java.util.concurrent.atomic.LongAdder; +import java.util.concurrent.locks.AbstractQueuedLongSynchronizer; + +@SuppressWarnings("serial") +public final class XbibSynchronizer extends AbstractQueuedLongSynchronizer { + + private final LongAdder counter = new LongAdder(); + + @Override + protected boolean tryAcquire(long value) { + return counter.longValue() > value; + } + + @Override + protected boolean tryRelease(long releases) { + counter.add(releases); + return true; + } + + public long getStamp() { + return counter.sum(); + } + + public void releaseConditional() { + if (hasQueuedThreads()) { + release(1); + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/CallableStatementWrapper.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/CallableStatementWrapper.java new file mode 100644 index 0000000..11c7875 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/CallableStatementWrapper.java @@ -0,0 +1,1775 @@ +package org.xbib.jdbc.pool.wrapper; + +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLType; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; +import org.xbib.jdbc.pool.util.AutoCloseableElement; +import static java.lang.reflect.Proxy.newProxyInstance; + +public final class CallableStatementWrapper extends StatementWrapper implements CallableStatement { + + static final String CLOSED_CALLABLE_STATEMENT_STRING = CallableStatementWrapper.class.getSimpleName() + ".CLOSED_STATEMENT"; + + private static final InvocationHandler CLOSED_HANDLER = new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return switch (method.getName()) { + case "close" -> Void.TYPE; + case "isClosed" -> Boolean.TRUE; + case "toString" -> CLOSED_CALLABLE_STATEMENT_STRING; + default -> throw new SQLException("CallableStatement is closed"); + }; + } + }; + + private static final CallableStatement CLOSED_STATEMENT = + (CallableStatement) newProxyInstance(CallableStatement.class.getClassLoader(), new Class[]{CallableStatement.class}, CLOSED_HANDLER); + + private CallableStatement wrappedStatement; + + public CallableStatementWrapper(ConnectionWrapper connectionWrapper, CallableStatement statement, boolean trackJdbcResources, AutoCloseableElement head) { + super(connectionWrapper, statement, trackJdbcResources, head); + wrappedStatement = statement; + } + + @Override + public void close() throws SQLException { + wrappedStatement = CLOSED_STATEMENT; + super.close(); + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException { + try { + wrappedStatement.registerOutParameter(parameterIndex, sqlType); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException { + try { + wrappedStatement.registerOutParameter(parameterIndex, sqlType, scale); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean wasNull() throws SQLException { + try { + return wrappedStatement.wasNull(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public String getString(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getString(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean getBoolean(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getBoolean(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public byte getByte(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getByte(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public short getShort(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getShort(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int getInt(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getInt(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long getLong(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getLong(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public float getFloat(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getFloat(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public double getDouble(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getDouble(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + @SuppressWarnings("deprecation") + public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException { + try { + return wrappedStatement.getBigDecimal(parameterIndex, scale); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public byte[] getBytes(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getBytes(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Date getDate(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getDate(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Time getTime(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getTime(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Timestamp getTimestamp(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getTimestamp(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Object getObject(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getObject(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public BigDecimal getBigDecimal(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getBigDecimal(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Object getObject(int parameterIndex, Map> map) throws SQLException { + try { + return wrappedStatement.getObject(parameterIndex, map); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Ref getRef(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getRef(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Blob getBlob(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getBlob(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Clob getClob(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getClob(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Array getArray(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getArray(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Date getDate(int parameterIndex, Calendar cal) throws SQLException { + try { + return wrappedStatement.getDate(parameterIndex, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Time getTime(int parameterIndex, Calendar cal) throws SQLException { + try { + return wrappedStatement.getTime(parameterIndex, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException { + try { + return wrappedStatement.getTimestamp(parameterIndex, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void registerOutParameter(int parameterIndex, int sqlType, String typeName) throws SQLException { + try { + wrappedStatement.registerOutParameter(parameterIndex, sqlType, typeName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void registerOutParameter(String parameterName, int sqlType) throws SQLException { + try { + wrappedStatement.registerOutParameter(parameterName, sqlType); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, int scale) throws SQLException { + try { + wrappedStatement.registerOutParameter(parameterName, sqlType, scale); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void registerOutParameter(String parameterName, int sqlType, String typeName) throws SQLException { + try { + wrappedStatement.registerOutParameter(parameterName, sqlType, typeName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public URL getURL(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getURL(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setURL(String parameterName, URL val) throws SQLException { + try { + wrappedStatement.setURL(parameterName, val); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNull(String parameterName, int sqlType) throws SQLException { + try { + wrappedStatement.setNull(parameterName, sqlType); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBoolean(String parameterName, boolean x) throws SQLException { + try { + wrappedStatement.setBoolean(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setByte(String parameterName, byte x) throws SQLException { + try { + wrappedStatement.setByte(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setShort(String parameterName, short x) throws SQLException { + try { + wrappedStatement.setShort(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setInt(String parameterName, int x) throws SQLException { + try { + wrappedStatement.setInt(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setLong(String parameterName, long x) throws SQLException { + try { + wrappedStatement.setLong(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setFloat(String parameterName, float x) throws SQLException { + try { + wrappedStatement.setFloat(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setDouble(String parameterName, double x) throws SQLException { + try { + wrappedStatement.setDouble(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException { + try { + wrappedStatement.setBigDecimal(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setString(String parameterName, String x) throws SQLException { + try { + wrappedStatement.setString(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBytes(String parameterName, byte[] x) throws SQLException { + try { + wrappedStatement.setBytes(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setDate(String parameterName, Date x) throws SQLException { + try { + wrappedStatement.setDate(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTime(String parameterName, Time x) throws SQLException { + try { + wrappedStatement.setTime(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTimestamp(String parameterName, Timestamp x) throws SQLException { + try { + wrappedStatement.setTimestamp(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException { + try { + wrappedStatement.setAsciiStream(parameterName, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException { + try { + wrappedStatement.setBinaryStream(parameterName, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException { + try { + wrappedStatement.setObject(parameterName, x, targetSqlType, scale); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException { + try { + wrappedStatement.setObject(parameterName, x, targetSqlType); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(String parameterName, Object x) throws SQLException { + try { + wrappedStatement.setObject(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, int length) throws SQLException { + try { + wrappedStatement.setCharacterStream(parameterName, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setDate(String parameterName, Date x, Calendar cal) throws SQLException { + try { + wrappedStatement.setDate(parameterName, x, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTime(String parameterName, Time x, Calendar cal) throws SQLException { + try { + wrappedStatement.setTime(parameterName, x, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException { + try { + wrappedStatement.setTimestamp(parameterName, x, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNull(String parameterName, int sqlType, String typeName) throws SQLException { + try { + wrappedStatement.setNull(parameterName, sqlType, typeName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public String getString(String parameterName) throws SQLException { + try { + return wrappedStatement.getString(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean getBoolean(String parameterName) throws SQLException { + try { + return wrappedStatement.getBoolean(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public byte getByte(String parameterName) throws SQLException { + try { + return wrappedStatement.getByte(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public short getShort(String parameterName) throws SQLException { + try { + return wrappedStatement.getShort(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int getInt(String parameterName) throws SQLException { + try { + return wrappedStatement.getInt(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long getLong(String parameterName) throws SQLException { + try { + return wrappedStatement.getLong(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public float getFloat(String parameterName) throws SQLException { + try { + return wrappedStatement.getFloat(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public double getDouble(String parameterName) throws SQLException { + try { + return wrappedStatement.getDouble(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public byte[] getBytes(String parameterName) throws SQLException { + try { + return wrappedStatement.getBytes(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Date getDate(String parameterName) throws SQLException { + try { + return wrappedStatement.getDate(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Time getTime(String parameterName) throws SQLException { + try { + return wrappedStatement.getTime(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Timestamp getTimestamp(String parameterName) throws SQLException { + try { + return wrappedStatement.getTimestamp(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Object getObject(String parameterName) throws SQLException { + try { + return wrappedStatement.getObject(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public BigDecimal getBigDecimal(String parameterName) throws SQLException { + try { + return wrappedStatement.getBigDecimal(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Object getObject(String parameterName, Map> map) throws SQLException { + try { + return wrappedStatement.getObject(parameterName, map); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Ref getRef(String parameterName) throws SQLException { + try { + return wrappedStatement.getRef(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Blob getBlob(String parameterName) throws SQLException { + try { + return wrappedStatement.getBlob(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Clob getClob(String parameterName) throws SQLException { + try { + return wrappedStatement.getClob(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Array getArray(String parameterName) throws SQLException { + try { + return wrappedStatement.getArray(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Date getDate(String parameterName, Calendar cal) throws SQLException { + try { + return wrappedStatement.getDate(parameterName, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Time getTime(String parameterName, Calendar cal) throws SQLException { + try { + return wrappedStatement.getTime(parameterName, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException { + try { + return wrappedStatement.getTimestamp(parameterName, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public URL getURL(String parameterName) throws SQLException { + try { + return wrappedStatement.getURL(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public RowId getRowId(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getRowId(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public RowId getRowId(String parameterName) throws SQLException { + try { + return wrappedStatement.getRowId(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setRowId(String parameterName, RowId x) throws SQLException { + try { + wrappedStatement.setRowId(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNString(String parameterName, String value) throws SQLException { + try { + wrappedStatement.setNString(parameterName, value); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException { + try { + wrappedStatement.setNCharacterStream(parameterName, value, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNClob(String parameterName, NClob value) throws SQLException { + try { + wrappedStatement.setNClob(parameterName, value); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setClob(String parameterName, Reader reader, long length) throws SQLException { + try { + wrappedStatement.setClob(parameterName, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException { + try { + wrappedStatement.setBlob(parameterName, inputStream, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNClob(String parameterName, Reader reader, long length) throws SQLException { + try { + wrappedStatement.setNClob(parameterName, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public NClob getNClob(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getNClob(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public NClob getNClob(String parameterName) throws SQLException { + try { + return wrappedStatement.getNClob(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException { + try { + wrappedStatement.setSQLXML(parameterName, xmlObject); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public SQLXML getSQLXML(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getSQLXML(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public SQLXML getSQLXML(String parameterName) throws SQLException { + try { + return wrappedStatement.getSQLXML(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public String getNString(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getNString(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public String getNString(String parameterName) throws SQLException { + try { + return wrappedStatement.getNString(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Reader getNCharacterStream(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getNCharacterStream(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Reader getNCharacterStream(String parameterName) throws SQLException { + try { + return wrappedStatement.getNCharacterStream(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Reader getCharacterStream(int parameterIndex) throws SQLException { + try { + return wrappedStatement.getCharacterStream(parameterIndex); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Reader getCharacterStream(String parameterName) throws SQLException { + try { + return wrappedStatement.getCharacterStream(parameterName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBlob(String parameterName, Blob x) throws SQLException { + try { + wrappedStatement.setBlob(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setClob(String parameterName, Clob x) throws SQLException { + try { + wrappedStatement.setClob(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException { + try { + wrappedStatement.setAsciiStream(parameterName, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException { + try { + wrappedStatement.setBinaryStream(parameterName, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException { + try { + wrappedStatement.setCharacterStream(parameterName, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setAsciiStream(String parameterName, InputStream x) throws SQLException { + try { + wrappedStatement.setAsciiStream(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBinaryStream(String parameterName, InputStream x) throws SQLException { + try { + wrappedStatement.setBinaryStream(parameterName, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setCharacterStream(String parameterName, Reader reader) throws SQLException { + try { + wrappedStatement.setCharacterStream(parameterName, reader); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNCharacterStream(String parameterName, Reader value) throws SQLException { + try { + wrappedStatement.setNCharacterStream(parameterName, value); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setClob(String parameterName, Reader reader) throws SQLException { + try { + wrappedStatement.setClob(parameterName, reader); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBlob(String parameterName, InputStream inputStream) throws SQLException { + try { + wrappedStatement.setBlob(parameterName, inputStream); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNClob(String parameterName, Reader reader) throws SQLException { + try { + wrappedStatement.setClob(parameterName, reader); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public T getObject(int parameterIndex, Class type) throws SQLException { + try { + return wrappedStatement.getObject(parameterIndex, type); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public T getObject(String parameterName, Class type) throws SQLException { + try { + return wrappedStatement.getObject(parameterName, type); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public ResultSet executeQuery() throws SQLException { + try { + return trackResultSet(wrappedStatement.executeQuery()); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int executeUpdate() throws SQLException { + try { + return wrappedStatement.executeUpdate(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + try { + wrappedStatement.setNull(parameterIndex, sqlType); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + try { + wrappedStatement.setBoolean(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + try { + wrappedStatement.setByte(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + try { + wrappedStatement.setShort(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + try { + wrappedStatement.setInt(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + try { + wrappedStatement.setLong(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + try { + wrappedStatement.setFloat(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + try { + wrappedStatement.setDouble(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + try { + wrappedStatement.setBigDecimal(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setString(int parameterIndex, String x) throws SQLException { + try { + wrappedStatement.setString(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + try { + wrappedStatement.setBytes(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + try { + wrappedStatement.setDate(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + try { + wrappedStatement.setTime(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + try { + wrappedStatement.setTimestamp(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + try { + wrappedStatement.setAsciiStream(parameterIndex, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + @SuppressWarnings("deprecation") + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + try { + wrappedStatement.setUnicodeStream(parameterIndex, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + try { + wrappedStatement.setBinaryStream(parameterIndex, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void clearParameters() throws SQLException { + try { + wrappedStatement.clearParameters(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + try { + wrappedStatement.setObject(parameterIndex, x, targetSqlType); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + try { + wrappedStatement.setObject(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean execute() throws SQLException { + try { + return wrappedStatement.execute(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void addBatch() throws SQLException { + try { + wrappedStatement.addBatch(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + try { + wrappedStatement.setCharacterStream(parameterIndex, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + try { + wrappedStatement.setRef(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + try { + wrappedStatement.setBlob(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + try { + wrappedStatement.setClob(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + try { + wrappedStatement.setArray(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + try { + return wrappedStatement.getMetaData(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + try { + wrappedStatement.setDate(parameterIndex, x, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + try { + wrappedStatement.setTime(parameterIndex, x, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + try { + wrappedStatement.setTimestamp(parameterIndex, x, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + try { + wrappedStatement.setNull(parameterIndex, sqlType, typeName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + try { + wrappedStatement.setURL(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + try { + return wrappedStatement.getParameterMetaData(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + try { + wrappedStatement.setRowId(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + try { + wrappedStatement.setNString(parameterIndex, value); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + try { + wrappedStatement.setNCharacterStream(parameterIndex, value, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + try { + wrappedStatement.setNClob(parameterIndex, value); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + try { + wrappedStatement.setClob(parameterIndex, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + try { + wrappedStatement.setBlob(parameterIndex, inputStream, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + try { + wrappedStatement.setNClob(parameterIndex, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + try { + wrappedStatement.setSQLXML(parameterIndex, xmlObject); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + try { + wrappedStatement.setObject(parameterIndex, x, targetSqlType, scaleOrLength); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + try { + wrappedStatement.setAsciiStream(parameterIndex, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + try { + wrappedStatement.setBinaryStream(parameterIndex, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + try { + wrappedStatement.setCharacterStream(parameterIndex, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + try { + wrappedStatement.setAsciiStream(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + try { + wrappedStatement.setBinaryStream(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + try { + wrappedStatement.setCharacterStream(parameterIndex, reader); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + try { + wrappedStatement.setNCharacterStream(parameterIndex, value); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + try { + wrappedStatement.setClob(parameterIndex, reader); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + try { + wrappedStatement.setBlob(parameterIndex, inputStream); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + try { + wrappedStatement.setNClob(parameterIndex, reader); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + // --- JDBC 4.2 // + + @Override + public void setObject(int parameterIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { + try { + wrappedStatement.setObject(parameterIndex, x, targetSqlType, scaleOrLength); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(int parameterIndex, Object x, SQLType targetSqlType) throws SQLException { + try { + wrappedStatement.setObject(parameterIndex, x, targetSqlType); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long executeLargeUpdate() throws SQLException { + try { + return wrappedStatement.executeLargeUpdate(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/ConnectionWrapper.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/ConnectionWrapper.java new file mode 100644 index 0000000..d0d11a4 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/ConnectionWrapper.java @@ -0,0 +1,805 @@ +package org.xbib.jdbc.pool.wrapper; + +import java.lang.reflect.InvocationHandler; +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; +import org.xbib.jdbc.pool.ConnectionHandler; +import org.xbib.jdbc.pool.util.AutoCloseableElement; +import static java.lang.reflect.Proxy.newProxyInstance; + +public final class ConnectionWrapper extends AutoCloseableElement implements Connection { + + private static final String CLOSED_HANDLER_STRING = ConnectionWrapper.class.getSimpleName() + ".CLOSED_CONNECTION"; + + private static final InvocationHandler CLOSED_HANDLER = (proxy, method, args) -> switch (method.getName()) { + case "abort", "close" -> Void.TYPE; + case "isClosed" -> Boolean.TRUE; + case "isValid" -> Boolean.FALSE; + case "toString" -> CLOSED_HANDLER_STRING; + default -> throw new SQLException("Connection is closed"); + }; + + private static final Connection CLOSED_CONNECTION = + (Connection) newProxyInstance(Connection.class.getClassLoader(), new Class[]{Connection.class}, CLOSED_HANDLER); + + private static final JdbcResourcesLeakReport JDBC_RESOURCES_NOT_LEAKED = new JdbcResourcesLeakReport(0, 0); + + // Connection.close() does not return the connection to the pool. + private final boolean detached; + + // Collection of Statements to close them on close(). If null Statements are not tracked. + private final AutoCloseableElement trackedStatements; + private final ConnectionHandler handler; + private int leakedResultSets; + private Connection wrappedConnection; + + public ConnectionWrapper(ConnectionHandler connectionHandler, boolean trackResources) { + this(connectionHandler, trackResources, false); + } + + public ConnectionWrapper(ConnectionHandler connectionHandler, boolean trackResources, boolean detached) { + super(null); + handler = connectionHandler; + wrappedConnection = connectionHandler.rawConnection(); + trackedStatements = trackResources ? AutoCloseableElement.newHead() : null; + this.detached = detached; + } + + public ConnectionWrapper(ConnectionHandler connectionHandler, boolean trackResources, AutoCloseableElement head) { + super(head); + handler = connectionHandler; + wrappedConnection = connectionHandler.rawConnection(); + trackedStatements = trackResources ? AutoCloseableElement.newHead() : null; + detached = false; + } + + public ConnectionHandler getHandler() { + return handler; + } + + public boolean isDetached() { + return detached; + } + + private Statement trackStatement(Statement statement) { + if (trackedStatements != null && statement != null) { + return new StatementWrapper(this, statement, true, trackedStatements); + } + return statement; + } + + private CallableStatement trackCallableStatement(CallableStatement statement) { + if (trackedStatements != null && statement != null) { + return new CallableStatementWrapper(this, statement, true, trackedStatements); + } + return statement; + } + + private PreparedStatement trackPreparedStatement(PreparedStatement statement) { + if (trackedStatements != null && statement != null) { + return new PreparedStatementWrapper(this, statement, true, trackedStatements); + } + return statement; + } + + private JdbcResourcesLeakReport closeTrackedStatements() throws SQLException { + if (trackedStatements != null) { + return new JdbcResourcesLeakReport(trackedStatements.closeAllAutocloseableElements(), leakedResultSets); + } + return JDBC_RESOURCES_NOT_LEAKED; + } + + @Override + public void close() throws SQLException { + handler.traceConnectionOperation("close()"); + if (wrappedConnection != CLOSED_CONNECTION) { + wrappedConnection = CLOSED_CONNECTION; + handler.onConnectionWrapperClose(this, closeTrackedStatements()); + } + } + + @Override + public void abort(Executor executor) throws SQLException { + handler.traceConnectionOperation("abort()"); + try { + wrappedConnection = CLOSED_CONNECTION; + wrappedConnection.abort(executor); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public boolean getAutoCommit() throws SQLException { + try { + handler.traceConnectionOperation("getAutoCommit()"); + handler.verifyEnlistment(); + return wrappedConnection.getAutoCommit(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + handler.traceConnectionOperation("setAutoCommit(boolean)"); + if (autoCommit && handler.isEnlisted()) { + handler.setFlushOnly(); + throw new SQLException("Trying to set autocommit in connection taking part of transaction"); + } + try { + handler.verifyEnlistment(); + if (wrappedConnection.getAutoCommit() != autoCommit) { + handler.setDirtyAttribute(ConnectionHandler.DirtyAttribute.AUTOCOMMIT); + wrappedConnection.setAutoCommit(autoCommit); + } + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void commit() throws SQLException { + handler.traceConnectionOperation("commit()"); + if (handler.isEnlisted()) { + handler.setFlushOnly(); + throw new SQLException("Attempting to commit while taking part in a transaction"); + } + try { + wrappedConnection.commit(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void rollback() throws SQLException { + handler.traceConnectionOperation("rollback()"); + if (handler.isEnlisted()) { + handler.setFlushOnly(); + throw new SQLException("Attempting to rollback while enlisted in a transaction"); + } + try { + handler.verifyEnlistment(); + wrappedConnection.rollback(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void rollback(Savepoint savepoint) throws SQLException { + handler.traceConnectionOperation("rollback(Savepoint)"); + if (handler.isEnlisted()) { + handler.setFlushOnly(); + throw new SQLException("Attempting to rollback while enlisted in a transaction"); + } + try { + handler.verifyEnlistment(); + wrappedConnection.rollback(savepoint); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void clearWarnings() throws SQLException { + try { + handler.traceConnectionOperation("clearWarnings()"); + handler.verifyEnlistment(); + wrappedConnection.clearWarnings(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Clob createClob() throws SQLException { + try { + handler.traceConnectionOperation("createClob()"); + handler.verifyEnlistment(); + return wrappedConnection.createClob(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Blob createBlob() throws SQLException { + try { + handler.traceConnectionOperation("createBlob()"); + handler.verifyEnlistment(); + return wrappedConnection.createBlob(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public NClob createNClob() throws SQLException { + try { + handler.traceConnectionOperation("createNClob()"); + handler.verifyEnlistment(); + return wrappedConnection.createNClob(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public SQLXML createSQLXML() throws SQLException { + try { + handler.traceConnectionOperation("createSQLXML()"); + handler.verifyEnlistment(); + return wrappedConnection.createSQLXML(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + try { + handler.traceConnectionOperation("createArrayOf(String, Object[])"); + handler.verifyEnlistment(); + return wrappedConnection.createArrayOf(typeName, elements); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Statement createStatement() throws SQLException { + try { + handler.traceConnectionOperation("createStatement()"); + handler.verifyEnlistment(); + return trackStatement(wrappedConnection.createStatement()); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + try { + handler.traceConnectionOperation("createStatement(int, int)"); + handler.verifyEnlistment(); + return trackStatement(wrappedConnection.createStatement(resultSetType, resultSetConcurrency)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + try { + handler.traceConnectionOperation("createStatement(int, int, int)"); + handler.verifyEnlistment(); + return trackStatement(wrappedConnection.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + try { + handler.traceConnectionOperation("createStruct(String, Object[])"); + handler.verifyEnlistment(); + return wrappedConnection.createStruct(typeName, attributes); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public String getCatalog() throws SQLException { + try { + handler.traceConnectionOperation("getCatalog()"); + handler.verifyEnlistment(); + return wrappedConnection.getCatalog(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void setCatalog(String catalog) throws SQLException { + try { + handler.traceConnectionOperation("setCatalog(String)"); + handler.verifyEnlistment(); + handler.setDirtyAttribute(ConnectionHandler.DirtyAttribute.CATALOG); + wrappedConnection.setCatalog(catalog); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public int getHoldability() throws SQLException { + try { + handler.traceConnectionOperation("getHoldability()"); + handler.verifyEnlistment(); + return wrappedConnection.getHoldability(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void setHoldability(int holdability) throws SQLException { + try { + handler.traceConnectionOperation("setHoldability(int)"); + handler.verifyEnlistment(); + wrappedConnection.setHoldability(holdability); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Properties getClientInfo() throws SQLException { + try { + handler.traceConnectionOperation("getClientInfo()"); + handler.verifyEnlistment(); + return wrappedConnection.getClientInfo(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + try { + handler.traceConnectionOperation("setClientInfo(Properties)"); + wrappedConnection.setClientInfo(properties); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public String getClientInfo(String name) throws SQLException { + try { + handler.traceConnectionOperation("getClientInfo(String)"); + handler.verifyEnlistment(); + return wrappedConnection.getClientInfo(name); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + try { + handler.traceConnectionOperation("getMetaData()"); + handler.verifyEnlistment(); + return wrappedConnection.getMetaData(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public int getNetworkTimeout() throws SQLException { + try { + handler.traceConnectionOperation("getNetworkTimeout()"); + handler.verifyEnlistment(); + return wrappedConnection.getNetworkTimeout(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public String getSchema() throws SQLException { + try { + handler.traceConnectionOperation("getSchema()"); + handler.verifyEnlistment(); + return wrappedConnection.getSchema(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void setSchema(String schema) throws SQLException { + try { + handler.traceConnectionOperation("setSchema(String)"); + handler.verifyEnlistment(); + handler.setDirtyAttribute(ConnectionHandler.DirtyAttribute.SCHEMA); + wrappedConnection.setSchema(schema); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Map> getTypeMap() throws SQLException { + try { + handler.traceConnectionOperation("getTypeMap()"); + handler.verifyEnlistment(); + return wrappedConnection.getTypeMap(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + try { + handler.traceConnectionOperation("setTypeMap(Map>)"); + handler.verifyEnlistment(); + wrappedConnection.setTypeMap(map); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public int getTransactionIsolation() throws SQLException { + try { + handler.traceConnectionOperation("getTransactionIsolation()"); + handler.verifyEnlistment(); + return wrappedConnection.getTransactionIsolation(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + try { + handler.traceConnectionOperation("setTransactionIsolation(int)"); + handler.verifyEnlistment(); + handler.setDirtyAttribute(ConnectionHandler.DirtyAttribute.TRANSACTION_ISOLATION); + wrappedConnection.setTransactionIsolation(level); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public SQLWarning getWarnings() throws SQLException { + try { + handler.traceConnectionOperation("getWarnings()"); + handler.verifyEnlistment(); + return wrappedConnection.getWarnings(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public boolean isClosed() throws SQLException { + try { + handler.traceConnectionOperation("isClosed()"); + return wrappedConnection.isClosed(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public boolean isReadOnly() throws SQLException { + try { + handler.traceConnectionOperation("isReadOnly()"); + handler.verifyEnlistment(); + return wrappedConnection.isReadOnly(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + try { + handler.traceConnectionOperation("setReadOnly(boolean)"); + handler.verifyEnlistment(); + wrappedConnection.setReadOnly(readOnly); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public boolean isValid(int timeout) throws SQLException { + try { + handler.traceConnectionOperation("isValid(int)"); + handler.verifyEnlistment(); + return wrappedConnection.isValid(timeout); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public String nativeSQL(String sql) throws SQLException { + try { + handler.traceConnectionOperation("nativeSQL(String)"); + handler.verifyEnlistment(); + return wrappedConnection.nativeSQL(sql); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + try { + handler.traceConnectionOperation("prepareCall(String)"); + handler.verifyEnlistment(); + return trackCallableStatement(wrappedConnection.prepareCall(sql)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + try { + handler.traceConnectionOperation("prepareCall(String, int, int)"); + handler.verifyEnlistment(); + return trackCallableStatement(wrappedConnection.prepareCall(sql, resultSetType, resultSetConcurrency)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + try { + handler.traceConnectionOperation("prepareCall(String, int, int, int)"); + handler.verifyEnlistment(); + return trackCallableStatement(wrappedConnection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + try { + handler.traceConnectionOperation("prepareStatement(String)"); + handler.verifyEnlistment(); + return trackPreparedStatement(wrappedConnection.prepareStatement(sql)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + try { + handler.traceConnectionOperation("prepareStatement(String, int, int)"); + handler.verifyEnlistment(); + return trackPreparedStatement(wrappedConnection.prepareStatement(sql, resultSetType, resultSetConcurrency)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + try { + handler.traceConnectionOperation("prepareStatement(String, int, int, int)"); + handler.verifyEnlistment(); + return trackPreparedStatement(wrappedConnection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + try { + handler.traceConnectionOperation("prepareStatement(String, int)"); + handler.verifyEnlistment(); + return trackPreparedStatement(wrappedConnection.prepareStatement(sql, autoGeneratedKeys)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + try { + handler.traceConnectionOperation("prepareStatement(String, int[])"); + handler.verifyEnlistment(); + return trackPreparedStatement(wrappedConnection.prepareStatement(sql, columnIndexes)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + try { + handler.traceConnectionOperation("prepareStatement(String, String[])"); + handler.verifyEnlistment(); + return trackPreparedStatement(wrappedConnection.prepareStatement(sql, columnNames)); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + try { + handler.traceConnectionOperation("releaseSavepoint(Savepoint)"); + handler.verifyEnlistment(); + wrappedConnection.releaseSavepoint(savepoint); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + try { + handler.traceConnectionOperation("setClientInfo(String, String)"); + wrappedConnection.setClientInfo(name, value); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Savepoint setSavepoint() throws SQLException { + try { + handler.traceConnectionOperation("setSavepoint()"); + handler.verifyEnlistment(); + return wrappedConnection.setSavepoint(); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Savepoint setSavepoint(String name) throws SQLException { + try { + handler.traceConnectionOperation("setSavepoint(String)"); + handler.verifyEnlistment(); + return wrappedConnection.setSavepoint(name); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + try { + handler.traceConnectionOperation("setNetworkTimeout(Executor, int)"); + handler.verifyEnlistment(); + handler.setDirtyAttribute(ConnectionHandler.DirtyAttribute.NETWORK_TIMEOUT); + wrappedConnection.setNetworkTimeout(executor, milliseconds); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + // --- // + + @Override + public T unwrap(Class target) throws SQLException { + try { + handler.traceConnectionOperation("unwrap(Class)"); + return wrappedConnection.unwrap(target); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public boolean isWrapperFor(Class target) throws SQLException { + try { + handler.traceConnectionOperation("isWrapperFor(Class)"); + return wrappedConnection.isWrapperFor(target); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public String toString() { + return "wrapped[" + wrappedConnection + (handler.isEnlisted() ? "]< switch (method.getName()) { + case "close" -> Void.TYPE; + case "isClosed" -> Boolean.TRUE; + case "toString" -> CLOSED_PREPARED_STATEMENT_STRING; + default -> throw new SQLException("CallableStatement is closed"); + }; + + private static final PreparedStatement CLOSED_STATEMENT = + (PreparedStatement) newProxyInstance(PreparedStatement.class.getClassLoader(), new Class[]{PreparedStatement.class}, CLOSED_HANDLER); + + private PreparedStatement wrappedStatement; + + public PreparedStatementWrapper(ConnectionWrapper connectionWrapper, PreparedStatement statement, boolean trackJdbcResources, AutoCloseableElement head) { + super(connectionWrapper, statement, trackJdbcResources, head); + wrappedStatement = statement; + } + + @Override + public void close() throws SQLException { + wrappedStatement = CLOSED_STATEMENT; + super.close(); + } + + @Override + public ResultSet executeQuery() throws SQLException { + try { + return trackResultSet(wrappedStatement.executeQuery()); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int executeUpdate() throws SQLException { + try { + return wrappedStatement.executeUpdate(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + try { + wrappedStatement.setNull(parameterIndex, sqlType); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + try { + wrappedStatement.setBoolean(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + try { + wrappedStatement.setByte(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + try { + wrappedStatement.setShort(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + try { + wrappedStatement.setInt(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + try { + wrappedStatement.setLong(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + try { + wrappedStatement.setFloat(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + try { + wrappedStatement.setDouble(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + try { + wrappedStatement.setBigDecimal(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setString(int parameterIndex, String x) throws SQLException { + try { + wrappedStatement.setString(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + try { + wrappedStatement.setBytes(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + try { + wrappedStatement.setDate(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + try { + wrappedStatement.setTime(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + try { + wrappedStatement.setTimestamp(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + try { + wrappedStatement.setAsciiStream(parameterIndex, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + @SuppressWarnings("deprecation") + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + try { + wrappedStatement.setUnicodeStream(parameterIndex, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + try { + wrappedStatement.setBinaryStream(parameterIndex, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void clearParameters() throws SQLException { + try { + wrappedStatement.clearParameters(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + try { + wrappedStatement.setObject(parameterIndex, x, targetSqlType); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + try { + wrappedStatement.setObject(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean execute() throws SQLException { + try { + return wrappedStatement.execute(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void addBatch() throws SQLException { + try { + wrappedStatement.addBatch(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException { + try { + wrappedStatement.setCharacterStream(parameterIndex, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + try { + wrappedStatement.setRef(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + try { + wrappedStatement.setBlob(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + try { + wrappedStatement.setClob(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + try { + wrappedStatement.setArray(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + try { + return wrappedStatement.getMetaData(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + try { + wrappedStatement.setDate(parameterIndex, x, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + try { + wrappedStatement.setTime(parameterIndex, x, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + try { + wrappedStatement.setTimestamp(parameterIndex, x, cal); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + try { + wrappedStatement.setNull(parameterIndex, sqlType, typeName); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + try { + wrappedStatement.setURL(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + try { + return wrappedStatement.getParameterMetaData(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + try { + wrappedStatement.setRowId(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + try { + wrappedStatement.setNString(parameterIndex, value); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException { + try { + wrappedStatement.setNCharacterStream(parameterIndex, value, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNClob(int parameterIndex, NClob value) throws SQLException { + try { + wrappedStatement.setNClob(parameterIndex, value); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setClob(int parameterIndex, Reader reader, long length) throws SQLException { + try { + wrappedStatement.setClob(parameterIndex, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException { + try { + wrappedStatement.setBlob(parameterIndex, inputStream, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException { + try { + wrappedStatement.setNClob(parameterIndex, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + try { + wrappedStatement.setSQLXML(parameterIndex, xmlObject); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + try { + wrappedStatement.setObject(parameterIndex, x, targetSqlType, scaleOrLength); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + try { + wrappedStatement.setAsciiStream(parameterIndex, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + try { + wrappedStatement.setBinaryStream(parameterIndex, x, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException { + try { + wrappedStatement.setCharacterStream(parameterIndex, reader, length); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + try { + wrappedStatement.setAsciiStream(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + try { + wrappedStatement.setBinaryStream(parameterIndex, x); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException { + try { + wrappedStatement.setCharacterStream(parameterIndex, reader); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException { + try { + wrappedStatement.setNCharacterStream(parameterIndex, value); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setClob(int parameterIndex, Reader reader) throws SQLException { + try { + wrappedStatement.setClob(parameterIndex, reader); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException { + try { + wrappedStatement.setBlob(parameterIndex, inputStream); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setNClob(int parameterIndex, Reader reader) throws SQLException { + try { + wrappedStatement.setNClob(parameterIndex, reader); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + // --- JDBC 4.2 // + + @Override + public void setObject(int parameterIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { + try { + wrappedStatement.setObject(parameterIndex, x, targetSqlType, scaleOrLength); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setObject(int parameterIndex, Object x, SQLType targetSqlType) throws SQLException { + try { + wrappedStatement.setObject(parameterIndex, x, targetSqlType); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long executeLargeUpdate() throws SQLException { + try { + return wrappedStatement.executeLargeUpdate(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/ResultSetWrapper.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/ResultSetWrapper.java new file mode 100644 index 0000000..2807921 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/ResultSetWrapper.java @@ -0,0 +1,2009 @@ +package org.xbib.jdbc.pool.wrapper; + +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.InvocationHandler; +import java.math.BigDecimal; +import java.net.URL; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLType; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Statement; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Map; +import org.xbib.jdbc.pool.util.AutoCloseableElement; +import static java.lang.reflect.Proxy.newProxyInstance; + +public final class ResultSetWrapper extends AutoCloseableElement implements ResultSet { + + static final String CLOSED_RESULT_SET_STRING = ResultSetWrapper.class.getSimpleName() + ".CLOSED_RESULT_SET"; + + private static final InvocationHandler CLOSED_HANDLER = (proxy, method, args) -> switch (method.getName()) { + case "close" -> Void.TYPE; + case "isClosed" -> Boolean.TRUE; + case "toString" -> CLOSED_RESULT_SET_STRING; + default -> throw new SQLException("ResultSet is closed"); + }; + + private static final ResultSet CLOSED_RESULT_SET = + (ResultSet) newProxyInstance(ResultSet.class.getClassLoader(), new Class[]{ResultSet.class}, CLOSED_HANDLER); + + private final StatementWrapper statement; + + private ResultSet wrappedResultSet; + + public ResultSetWrapper(StatementWrapper statementWrapper, ResultSet resultSet, AutoCloseableElement head) { + super(head); + statement = statementWrapper; + wrappedResultSet = resultSet; + } + + @Override + public void close() throws SQLException { + try { + wrappedResultSet.close(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } finally { + wrappedResultSet = CLOSED_RESULT_SET; + } + } + + // --- // + + @Override + public boolean next() throws SQLException { + try { + return wrappedResultSet.next(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean wasNull() throws SQLException { + try { + return wrappedResultSet.wasNull(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public String getString(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getString(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean getBoolean(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getBoolean(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public byte getByte(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getByte(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public short getShort(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getShort(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int getInt(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getInt(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long getLong(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getLong(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public float getFloat(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getFloat(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public double getDouble(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getDouble(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + @SuppressWarnings("deprecation") + public BigDecimal getBigDecimal(int columnIndex, int scale) throws SQLException { + try { + return wrappedResultSet.getBigDecimal(columnIndex, scale); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public byte[] getBytes(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getBytes(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Date getDate(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getDate(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Time getTime(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getTime(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Timestamp getTimestamp(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getTimestamp(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public InputStream getAsciiStream(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getAsciiStream(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + @SuppressWarnings("deprecation") + public InputStream getUnicodeStream(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getUnicodeStream(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public InputStream getBinaryStream(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getBinaryStream(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public String getString(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getString(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean getBoolean(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getBoolean(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public byte getByte(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getByte(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public short getShort(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getShort(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int getInt(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getInt(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long getLong(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getLong(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public float getFloat(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getFloat(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public double getDouble(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getDouble(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + @SuppressWarnings("deprecation") + public BigDecimal getBigDecimal(String columnLabel, int scale) throws SQLException { + try { + return wrappedResultSet.getBigDecimal(columnLabel, scale); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public byte[] getBytes(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getBytes(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Date getDate(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getDate(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Time getTime(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getTime(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Timestamp getTimestamp(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getTimestamp(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public InputStream getAsciiStream(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getAsciiStream(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + @SuppressWarnings("deprecation") + public InputStream getUnicodeStream(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getUnicodeStream(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public InputStream getBinaryStream(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getBinaryStream(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public SQLWarning getWarnings() throws SQLException { + try { + return wrappedResultSet.getWarnings(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void clearWarnings() throws SQLException { + try { + wrappedResultSet.clearWarnings(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public String getCursorName() throws SQLException { + try { + return wrappedResultSet.getCursorName(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + try { + return wrappedResultSet.getMetaData(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Object getObject(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getObject(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Object getObject(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getObject(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int findColumn(String columnLabel) throws SQLException { + try { + return wrappedResultSet.findColumn(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Reader getCharacterStream(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getCharacterStream(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Reader getCharacterStream(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getCharacterStream(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public BigDecimal getBigDecimal(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getBigDecimal(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public BigDecimal getBigDecimal(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getBigDecimal(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean isBeforeFirst() throws SQLException { + try { + return wrappedResultSet.isBeforeFirst(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean isAfterLast() throws SQLException { + try { + return wrappedResultSet.isAfterLast(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean isFirst() throws SQLException { + try { + return wrappedResultSet.isFirst(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean isLast() throws SQLException { + try { + return wrappedResultSet.isLast(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void beforeFirst() throws SQLException { + try { + wrappedResultSet.beforeFirst(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void afterLast() throws SQLException { + try { + wrappedResultSet.afterLast(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean first() throws SQLException { + try { + return wrappedResultSet.first(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean last() throws SQLException { + try { + return wrappedResultSet.last(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int getRow() throws SQLException { + try { + return wrappedResultSet.getRow(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean absolute(int row) throws SQLException { + try { + return wrappedResultSet.absolute(row); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean relative(int rows) throws SQLException { + try { + return wrappedResultSet.relative(rows); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean previous() throws SQLException { + try { + return wrappedResultSet.previous(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int getFetchDirection() throws SQLException { + try { + return wrappedResultSet.getFetchDirection(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + try { + wrappedResultSet.setFetchDirection(direction); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int getFetchSize() throws SQLException { + try { + return wrappedResultSet.getFetchSize(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setFetchSize(int rows) throws SQLException { + try { + wrappedResultSet.setFetchSize(rows); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int getType() throws SQLException { + try { + return wrappedResultSet.getType(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int getConcurrency() throws SQLException { + try { + return wrappedResultSet.getConcurrency(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean rowUpdated() throws SQLException { + try { + return wrappedResultSet.rowUpdated(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean rowInserted() throws SQLException { + try { + return wrappedResultSet.rowInserted(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean rowDeleted() throws SQLException { + try { + return wrappedResultSet.rowDeleted(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNull(int columnIndex) throws SQLException { + try { + wrappedResultSet.updateNull(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBoolean(int columnIndex, boolean x) throws SQLException { + try { + wrappedResultSet.updateBoolean(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateByte(int columnIndex, byte x) throws SQLException { + try { + wrappedResultSet.updateByte(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateShort(int columnIndex, short x) throws SQLException { + try { + wrappedResultSet.updateShort(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateInt(int columnIndex, int x) throws SQLException { + try { + wrappedResultSet.updateInt(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateLong(int columnIndex, long x) throws SQLException { + try { + wrappedResultSet.updateLong(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateFloat(int columnIndex, float x) throws SQLException { + try { + wrappedResultSet.updateFloat(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateDouble(int columnIndex, double x) throws SQLException { + try { + wrappedResultSet.updateDouble(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBigDecimal(int columnIndex, BigDecimal x) throws SQLException { + try { + wrappedResultSet.updateBigDecimal(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateString(int columnIndex, String x) throws SQLException { + try { + wrappedResultSet.updateString(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBytes(int columnIndex, byte[] x) throws SQLException { + try { + wrappedResultSet.updateBytes(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateDate(int columnIndex, Date x) throws SQLException { + try { + wrappedResultSet.updateDate(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateTime(int columnIndex, Time x) throws SQLException { + try { + wrappedResultSet.updateTime(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateTimestamp(int columnIndex, Timestamp x) throws SQLException { + try { + wrappedResultSet.updateTimestamp(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, int length) throws SQLException { + try { + wrappedResultSet.updateAsciiStream(columnIndex, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, int length) throws SQLException { + try { + wrappedResultSet.updateBinaryStream(columnIndex, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, int length) throws SQLException { + try { + wrappedResultSet.updateCharacterStream(columnIndex, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateObject(int columnIndex, Object x, int scaleOrLength) throws SQLException { + try { + wrappedResultSet.updateObject(columnIndex, x, scaleOrLength); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateObject(int columnIndex, Object x) throws SQLException { + try { + wrappedResultSet.updateObject(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNull(String columnLabel) throws SQLException { + try { + wrappedResultSet.updateNull(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBoolean(String columnLabel, boolean x) throws SQLException { + try { + wrappedResultSet.updateBoolean(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateByte(String columnLabel, byte x) throws SQLException { + try { + wrappedResultSet.updateByte(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateShort(String columnLabel, short x) throws SQLException { + try { + wrappedResultSet.updateShort(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateInt(String columnLabel, int x) throws SQLException { + try { + wrappedResultSet.updateInt(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateLong(String columnLabel, long x) throws SQLException { + try { + wrappedResultSet.updateLong(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateFloat(String columnLabel, float x) throws SQLException { + try { + wrappedResultSet.updateFloat(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateDouble(String columnLabel, double x) throws SQLException { + try { + wrappedResultSet.updateDouble(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBigDecimal(String columnLabel, BigDecimal x) throws SQLException { + try { + wrappedResultSet.updateBigDecimal(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateString(String columnLabel, String x) throws SQLException { + try { + wrappedResultSet.updateString(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBytes(String columnLabel, byte[] x) throws SQLException { + try { + wrappedResultSet.updateBytes(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateDate(String columnLabel, Date x) throws SQLException { + try { + wrappedResultSet.updateDate(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateTime(String columnLabel, Time x) throws SQLException { + try { + wrappedResultSet.updateTime(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateTimestamp(String columnLabel, Timestamp x) throws SQLException { + try { + wrappedResultSet.updateTimestamp(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, int length) throws SQLException { + try { + wrappedResultSet.updateAsciiStream(columnLabel, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, int length) throws SQLException { + try { + wrappedResultSet.updateBinaryStream(columnLabel, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, int length) throws SQLException { + try { + wrappedResultSet.updateCharacterStream(columnLabel, reader, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateObject(String columnLabel, Object x, int scaleOrLength) throws SQLException { + try { + wrappedResultSet.updateObject(columnLabel, x, scaleOrLength); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateObject(String columnLabel, Object x) throws SQLException { + try { + wrappedResultSet.updateObject(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void insertRow() throws SQLException { + try { + wrappedResultSet.insertRow(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateRow() throws SQLException { + try { + wrappedResultSet.updateRow(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void deleteRow() throws SQLException { + try { + wrappedResultSet.deleteRow(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void refreshRow() throws SQLException { + try { + wrappedResultSet.refreshRow(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void cancelRowUpdates() throws SQLException { + try { + wrappedResultSet.cancelRowUpdates(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void moveToInsertRow() throws SQLException { + try { + wrappedResultSet.moveToInsertRow(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void moveToCurrentRow() throws SQLException { + try { + wrappedResultSet.moveToCurrentRow(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Statement getStatement() throws SQLException { + return statement; + } + + @Override + public Object getObject(int columnIndex, Map> map) throws SQLException { + try { + return wrappedResultSet.getObject(columnIndex, map); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Ref getRef(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getRef(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Blob getBlob(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getBlob(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Clob getClob(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getClob(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Array getArray(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getArray(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Object getObject(String columnLabel, Map> map) throws SQLException { + try { + return wrappedResultSet.getObject(columnLabel, map); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Ref getRef(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getRef(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Blob getBlob(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getBlob(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Clob getClob(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getClob(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Array getArray(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getArray(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Date getDate(int columnIndex, Calendar cal) throws SQLException { + try { + return wrappedResultSet.getDate(columnIndex, cal); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Date getDate(String columnLabel, Calendar cal) throws SQLException { + try { + return wrappedResultSet.getDate(columnLabel, cal); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Time getTime(int columnIndex, Calendar cal) throws SQLException { + try { + return wrappedResultSet.getTime(columnIndex, cal); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Time getTime(String columnLabel, Calendar cal) throws SQLException { + try { + return wrappedResultSet.getTime(columnLabel, cal); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Timestamp getTimestamp(int columnIndex, Calendar cal) throws SQLException { + try { + return wrappedResultSet.getTimestamp(columnIndex, cal); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Timestamp getTimestamp(String columnLabel, Calendar cal) throws SQLException { + try { + return wrappedResultSet.getTimestamp(columnLabel, cal); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public URL getURL(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getURL(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public URL getURL(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getURL(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateRef(int columnIndex, Ref x) throws SQLException { + try { + wrappedResultSet.updateRef(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateRef(String columnLabel, Ref x) throws SQLException { + try { + wrappedResultSet.updateRef(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBlob(int columnIndex, Blob x) throws SQLException { + try { + wrappedResultSet.updateBlob(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBlob(String columnLabel, Blob x) throws SQLException { + try { + wrappedResultSet.updateBlob(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateClob(int columnIndex, Clob x) throws SQLException { + try { + wrappedResultSet.updateClob(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateClob(String columnLabel, Clob x) throws SQLException { + try { + wrappedResultSet.updateClob(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateArray(int columnIndex, Array x) throws SQLException { + try { + wrappedResultSet.updateArray(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateArray(String columnLabel, Array x) throws SQLException { + try { + wrappedResultSet.updateArray(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public RowId getRowId(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getRowId(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public RowId getRowId(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getRowId(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateRowId(int columnIndex, RowId x) throws SQLException { + try { + wrappedResultSet.updateRowId(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateRowId(String columnLabel, RowId x) throws SQLException { + try { + wrappedResultSet.updateRowId(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public int getHoldability() throws SQLException { + try { + return wrappedResultSet.getHoldability(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean isClosed() throws SQLException { + try { + return wrappedResultSet.isClosed(); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNString(int columnIndex, String nString) throws SQLException { + try { + wrappedResultSet.updateNString(columnIndex, nString); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNString(String columnLabel, String nString) throws SQLException { + try { + wrappedResultSet.updateNString(columnLabel, nString); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNClob(int columnIndex, NClob nClob) throws SQLException { + try { + wrappedResultSet.updateNClob(columnIndex, nClob); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNClob(String columnLabel, NClob nClob) throws SQLException { + try { + wrappedResultSet.updateNClob(columnLabel, nClob); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public NClob getNClob(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getNClob(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public NClob getNClob(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getNClob(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public SQLXML getSQLXML(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getSQLXML(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public SQLXML getSQLXML(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getSQLXML(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateSQLXML(int columnIndex, SQLXML xmlObject) throws SQLException { + try { + wrappedResultSet.updateSQLXML(columnIndex, xmlObject); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateSQLXML(String columnLabel, SQLXML xmlObject) throws SQLException { + try { + wrappedResultSet.updateSQLXML(columnLabel, xmlObject); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public String getNString(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getNString(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public String getNString(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getNString(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Reader getNCharacterStream(int columnIndex) throws SQLException { + try { + return wrappedResultSet.getNCharacterStream(columnIndex); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public Reader getNCharacterStream(String columnLabel) throws SQLException { + try { + return wrappedResultSet.getNCharacterStream(columnLabel); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + try { + wrappedResultSet.updateNCharacterStream(columnIndex, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + try { + wrappedResultSet.updateNCharacterStream(columnLabel, reader, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x, long length) throws SQLException { + try { + wrappedResultSet.updateAsciiStream(columnIndex, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x, long length) throws SQLException { + try { + wrappedResultSet.updateBinaryStream(columnIndex, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x, long length) throws SQLException { + try { + wrappedResultSet.updateCharacterStream(columnIndex, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x, long length) throws SQLException { + try { + wrappedResultSet.updateAsciiStream(columnLabel, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x, long length) throws SQLException { + try { + wrappedResultSet.updateBinaryStream(columnLabel, x, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader, long length) throws SQLException { + try { + wrappedResultSet.updateCharacterStream(columnLabel, reader, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream, long length) throws SQLException { + try { + wrappedResultSet.updateBlob(columnIndex, inputStream, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream, long length) throws SQLException { + try { + wrappedResultSet.updateBlob(columnLabel, inputStream, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateClob(int columnIndex, Reader reader, long length) throws SQLException { + try { + wrappedResultSet.updateClob(columnIndex, reader, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateClob(String columnLabel, Reader reader, long length) throws SQLException { + try { + wrappedResultSet.updateClob(columnLabel, reader, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNClob(int columnIndex, Reader reader, long length) throws SQLException { + try { + wrappedResultSet.updateNClob(columnIndex, reader, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNClob(String columnLabel, Reader reader, long length) throws SQLException { + try { + wrappedResultSet.updateClob(columnLabel, reader, length); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNCharacterStream(int columnIndex, Reader x) throws SQLException { + try { + wrappedResultSet.updateNCharacterStream(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNCharacterStream(String columnLabel, Reader reader) throws SQLException { + try { + wrappedResultSet.updateNCharacterStream(columnLabel, reader); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateAsciiStream(int columnIndex, InputStream x) throws SQLException { + try { + wrappedResultSet.updateAsciiStream(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBinaryStream(int columnIndex, InputStream x) throws SQLException { + try { + wrappedResultSet.updateBinaryStream(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateCharacterStream(int columnIndex, Reader x) throws SQLException { + try { + wrappedResultSet.updateCharacterStream(columnIndex, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateAsciiStream(String columnLabel, InputStream x) throws SQLException { + try { + wrappedResultSet.updateAsciiStream(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBinaryStream(String columnLabel, InputStream x) throws SQLException { + try { + wrappedResultSet.updateBinaryStream(columnLabel, x); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateCharacterStream(String columnLabel, Reader reader) throws SQLException { + try { + wrappedResultSet.updateCharacterStream(columnLabel, reader); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBlob(int columnIndex, InputStream inputStream) throws SQLException { + try { + wrappedResultSet.updateBlob(columnIndex, inputStream); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateBlob(String columnLabel, InputStream inputStream) throws SQLException { + try { + wrappedResultSet.updateBlob(columnLabel, inputStream); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateClob(int columnIndex, Reader reader) throws SQLException { + try { + wrappedResultSet.updateClob(columnIndex, reader); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateClob(String columnLabel, Reader reader) throws SQLException { + try { + wrappedResultSet.updateClob(columnLabel, reader); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNClob(int columnIndex, Reader reader) throws SQLException { + try { + wrappedResultSet.updateNClob(columnIndex, reader); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateNClob(String columnLabel, Reader reader) throws SQLException { + try { + wrappedResultSet.updateNClob(columnLabel, reader); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public T getObject(int columnIndex, Class type) throws SQLException { + try { + return wrappedResultSet.getObject(columnIndex, type); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public T getObject(String columnLabel, Class type) throws SQLException { + try { + return wrappedResultSet.getObject(columnLabel, type); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + // --- JDBC 4.2 // + + @Override + public void updateObject(int columnIndex, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { + try { + wrappedResultSet.updateObject(columnIndex, x, targetSqlType, scaleOrLength); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateObject(String columnLabel, Object x, SQLType targetSqlType, int scaleOrLength) throws SQLException { + try { + wrappedResultSet.updateObject(columnLabel, x, targetSqlType, scaleOrLength); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateObject(int columnIndex, Object x, SQLType targetSqlType) throws SQLException { + try { + wrappedResultSet.updateObject(columnIndex, x, targetSqlType); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void updateObject(String columnLabel, Object x, SQLType targetSqlType) throws SQLException { + try { + wrappedResultSet.updateObject(columnLabel, x, targetSqlType); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + // --- // + + @Override + public T unwrap(Class target) throws SQLException { + try { + return wrappedResultSet.unwrap(target); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public boolean isWrapperFor(Class target) throws SQLException { + try { + return wrappedResultSet.isWrapperFor(target); + } catch (SQLException se) { + statement.getConnectionWrapper().getHandler().setFlushOnly(se); + throw se; + } + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/StatementWrapper.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/StatementWrapper.java new file mode 100644 index 0000000..139eccf --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/StatementWrapper.java @@ -0,0 +1,584 @@ +package org.xbib.jdbc.pool.wrapper; + +import java.lang.reflect.InvocationHandler; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.Statement; +import org.xbib.jdbc.pool.util.AutoCloseableElement; +import static java.lang.reflect.Proxy.newProxyInstance; + +public class StatementWrapper extends AutoCloseableElement implements Statement { + + static final String CLOSED_STATEMENT_STRING = StatementWrapper.class.getSimpleName() + ".CLOSED_STATEMENT"; + + private static final InvocationHandler CLOSED_HANDLER = (proxy, method, args) -> switch (method.getName()) { + case "close" -> Void.TYPE; + case "isClosed" -> Boolean.TRUE; + case "toString" -> CLOSED_STATEMENT_STRING; + default -> throw new SQLException("Statement is closed"); + }; + + private static final Statement CLOSED_STATEMENT = + (Statement) newProxyInstance(Statement.class.getClassLoader(), new Class[]{Statement.class}, CLOSED_HANDLER); + + @SuppressWarnings("ProtectedField") + protected final ConnectionWrapper connection; + + // Collection of ResultSet to close them on close(). If null ResultSet are not tracked. + private final AutoCloseableElement trackedResultSets; + + private Statement wrappedStatement; + + public StatementWrapper(ConnectionWrapper connectionWrapper, Statement statement, boolean trackResources, AutoCloseableElement head) { + super(head); + connection = connectionWrapper; + wrappedStatement = statement; + trackedResultSets = trackResources ? AutoCloseableElement.newHead() : null; + } + + protected ResultSet trackResultSet(ResultSet resultSet) { + if (trackedResultSets != null && resultSet != null) { + return new ResultSetWrapper(this, resultSet, trackedResultSets); + } + return resultSet; + } + + private void closeTrackedResultSets() throws SQLException { + if (trackedResultSets != null) { + connection.addLeakedResultSets(trackedResultSets.closeAllAutocloseableElements()); + } + } + + ConnectionWrapper getConnectionWrapper() throws SQLException { + return connection; + } + + @Override + public void close() throws SQLException { + try { + if (wrappedStatement != CLOSED_STATEMENT) { + closeTrackedResultSets(); + wrappedStatement.close(); + } + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } finally { + wrappedStatement = CLOSED_STATEMENT; + } + } + + @Override + public final void clearWarnings() throws SQLException { + try { + wrappedStatement.clearWarnings(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final ResultSet executeQuery(String sql) throws SQLException { + try { + return trackResultSet(wrappedStatement.executeQuery(sql)); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int executeUpdate(String sql) throws SQLException { + try { + return wrappedStatement.executeUpdate(sql); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int getMaxFieldSize() throws SQLException { + try { + return wrappedStatement.getMaxFieldSize(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void setMaxFieldSize(int max) throws SQLException { + try { + wrappedStatement.setMaxFieldSize(max); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int getMaxRows() throws SQLException { + try { + return wrappedStatement.getMaxRows(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void setMaxRows(int max) throws SQLException { + try { + wrappedStatement.setMaxRows(max); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void setEscapeProcessing(boolean enable) throws SQLException { + try { + wrappedStatement.setEscapeProcessing(enable); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int getQueryTimeout() throws SQLException { + try { + return wrappedStatement.getQueryTimeout(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void setQueryTimeout(int seconds) throws SQLException { + try { + wrappedStatement.setQueryTimeout(seconds); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void cancel() throws SQLException { + try { + wrappedStatement.cancel(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void setCursorName(String name) throws SQLException { + try { + wrappedStatement.setCursorName(name); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final boolean execute(String sql) throws SQLException { + try { + return wrappedStatement.execute(sql); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final ResultSet getResultSet() throws SQLException { + try { + return trackResultSet(wrappedStatement.getResultSet()); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int getUpdateCount() throws SQLException { + try { + return wrappedStatement.getUpdateCount(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final boolean getMoreResults() throws SQLException { + try { + return wrappedStatement.getMoreResults(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int getFetchDirection() throws SQLException { + try { + return wrappedStatement.getFetchDirection(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void setFetchDirection(int direction) throws SQLException { + try { + wrappedStatement.setFetchDirection(direction); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int getFetchSize() throws SQLException { + try { + return wrappedStatement.getFetchSize(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void setFetchSize(int rows) throws SQLException { + try { + wrappedStatement.setFetchSize(rows); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int getResultSetConcurrency() throws SQLException { + try { + return wrappedStatement.getResultSetConcurrency(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int getResultSetType() throws SQLException { + try { + return wrappedStatement.getResultSetType(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void addBatch(String sql) throws SQLException { + try { + wrappedStatement.addBatch(sql); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void clearBatch() throws SQLException { + try { + wrappedStatement.clearBatch(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int[] executeBatch() throws SQLException { + try { + return wrappedStatement.executeBatch(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final Connection getConnection() throws SQLException { + return connection; + } + + @Override + public final boolean getMoreResults(int current) throws SQLException { + try { + return wrappedStatement.getMoreResults(current); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final ResultSet getGeneratedKeys() throws SQLException { + try { + return trackResultSet(wrappedStatement.getGeneratedKeys()); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + try { + return wrappedStatement.executeUpdate(sql, autoGeneratedKeys); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + try { + return wrappedStatement.executeUpdate(sql, columnIndexes); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int executeUpdate(String sql, String[] columnNames) throws SQLException { + try { + return wrappedStatement.executeUpdate(sql, columnNames); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + try { + return wrappedStatement.execute(sql, autoGeneratedKeys); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final boolean execute(String sql, int[] columnIndexes) throws SQLException { + try { + return wrappedStatement.execute(sql, columnIndexes); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final boolean execute(String sql, String[] columnNames) throws SQLException { + try { + return wrappedStatement.execute(sql, columnNames); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final int getResultSetHoldability() throws SQLException { + try { + return wrappedStatement.getResultSetHoldability(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final boolean isPoolable() throws SQLException { + try { + return wrappedStatement.isPoolable(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void setPoolable(boolean poolable) throws SQLException { + try { + wrappedStatement.setPoolable(poolable); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final void closeOnCompletion() throws SQLException { + try { + wrappedStatement.closeOnCompletion(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final boolean isCloseOnCompletion() throws SQLException { + try { + return wrappedStatement.isCloseOnCompletion(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final SQLWarning getWarnings() throws SQLException { + try { + return wrappedStatement.getWarnings(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final boolean isClosed() throws SQLException { + try { + return wrappedStatement.isClosed(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + // --- JDBC 4.2 // + + @Override + public long getLargeUpdateCount() throws SQLException { + try { + return wrappedStatement.getLargeUpdateCount(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long getLargeMaxRows() throws SQLException { + try { + return wrappedStatement.getLargeMaxRows(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public void setLargeMaxRows(long max) throws SQLException { + try { + wrappedStatement.setLargeMaxRows(max); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long[] executeLargeBatch() throws SQLException { + try { + return wrappedStatement.executeLargeBatch(); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long executeLargeUpdate(String sql) throws SQLException { + try { + return wrappedStatement.executeLargeUpdate(sql); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + try { + return wrappedStatement.executeLargeUpdate(sql, autoGeneratedKeys); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException { + try { + return wrappedStatement.executeLargeUpdate(sql, columnIndexes); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException { + try { + return wrappedStatement.executeLargeUpdate(sql, columnNames); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final T unwrap(Class target) throws SQLException { + try { + return wrappedStatement.unwrap(target); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final boolean isWrapperFor(Class target) throws SQLException { + try { + return wrappedStatement.isWrapperFor(target); + } catch (SQLException se) { + connection.getHandler().setFlushOnly(se); + throw se; + } + } + + @Override + public final String toString() { + return "wrapped[ " + wrappedStatement + " ]"; + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/XAConnectionWrapper.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/XAConnectionWrapper.java new file mode 100644 index 0000000..f63475d --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/XAConnectionWrapper.java @@ -0,0 +1,112 @@ +package org.xbib.jdbc.pool.wrapper; + +import java.lang.reflect.InvocationHandler; +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.ConnectionEventListener; +import javax.sql.StatementEventListener; +import javax.sql.XAConnection; +import javax.transaction.xa.XAResource; +import org.xbib.jdbc.pool.ConnectionHandler; +import org.xbib.jdbc.pool.util.AutoCloseableElement; +import static java.lang.reflect.Proxy.newProxyInstance; + +public final class XAConnectionWrapper extends AutoCloseableElement implements XAConnection { + + private static final String CLOSED_HANDLER_STRING = XAConnectionWrapper.class.getSimpleName() + ".CLOSED_XA_CONNECTION"; + + private static final InvocationHandler CLOSED_HANDLER = (proxy, method, args) -> switch (method.getName()) { + case "close" -> Void.TYPE; + case "isClosed" -> Boolean.TRUE; + case "toString" -> CLOSED_HANDLER_STRING; + default -> throw new SQLException("XAConnection is closed"); + }; + + private static final XAConnection CLOSED_XA_CONNECTION = + (XAConnection) newProxyInstance(XAConnection.class.getClassLoader(), new Class[]{XAConnection.class}, CLOSED_HANDLER); + + // Collection of XAResources to close them on close(). If null Statements are not tracked. + private final AutoCloseableElement trackedXAResources; + + private final ConnectionHandler handler; + + private XAConnection wrappedXAConnection; + + public XAConnectionWrapper(ConnectionHandler connectionHandler, XAConnection xaConnection, boolean trackResources) { + super(null); + handler = connectionHandler; + wrappedXAConnection = xaConnection; + trackedXAResources = trackResources ? newHead() : null; + } + + private XAResource trackXAResource(XAResource resource) { + if (trackedXAResources != null && resource != null) { + return new XAResourceWrapper(handler, resource, trackedXAResources); + } + return resource; + } + + @Override + public boolean isClosed() throws SQLException { + return wrappedXAConnection == CLOSED_XA_CONNECTION; + } + + @Override + public void close() throws SQLException { + handler.traceConnectionOperation("close()"); + if (wrappedXAConnection != CLOSED_XA_CONNECTION) { + wrappedXAConnection = CLOSED_XA_CONNECTION; + + trackedXAResources.closeAllAutocloseableElements(); + handler.transactionEnd(); + } + } + + @Override + public XAResource getXAResource() throws SQLException { + try { + handler.traceConnectionOperation("getXAResource()"); + handler.verifyEnlistment(); + return trackXAResource(handler.getXaResource()); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public Connection getConnection() throws SQLException { + try { + handler.traceConnectionOperation("getConnection()"); + handler.verifyEnlistment(); + return new ConnectionWrapper(handler, trackedXAResources != null, true); + } catch (SQLException se) { + handler.setFlushOnly(se); + throw se; + } + } + + @Override + public void addConnectionEventListener(ConnectionEventListener listener) { + handler.traceConnectionOperation("addConnectionEventListener()"); + wrappedXAConnection.addConnectionEventListener(listener); + } + + @Override + public void removeConnectionEventListener(ConnectionEventListener listener) { + handler.traceConnectionOperation("removeConnectionEventListener()"); + wrappedXAConnection.removeConnectionEventListener(listener); + } + + @Override + public void addStatementEventListener(StatementEventListener listener) { + handler.traceConnectionOperation("addStatementEventListener()"); + wrappedXAConnection.addStatementEventListener(listener); + } + + @Override + public void removeStatementEventListener(StatementEventListener listener) { + handler.traceConnectionOperation("removeStatementEventListener()"); + wrappedXAConnection.removeStatementEventListener(listener); + } +} diff --git a/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/XAResourceWrapper.java b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/XAResourceWrapper.java new file mode 100644 index 0000000..b5fed78 --- /dev/null +++ b/jdbc-pool/src/main/java/org/xbib/jdbc/pool/wrapper/XAResourceWrapper.java @@ -0,0 +1,107 @@ +package org.xbib.jdbc.pool.wrapper; + +import java.lang.reflect.InvocationHandler; +import java.sql.SQLException; +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; +import org.xbib.jdbc.pool.ConnectionHandler; +import org.xbib.jdbc.pool.util.AutoCloseableElement; +import static java.lang.reflect.Proxy.newProxyInstance; + +public class XAResourceWrapper extends AutoCloseableElement implements XAResource { + + private static final String CLOSED_HANDLER_STRING = XAResourceWrapper.class.getSimpleName() + ".CLOSED_XA_RESOURCE"; + + private static final InvocationHandler CLOSED_HANDLER = (proxy, method, args) -> switch (method.getName()) { + case "close" -> Void.TYPE; + case "isClosed" -> Boolean.TRUE; + case "toString" -> CLOSED_HANDLER_STRING; + default -> throw new SQLException("XAConnection for the XAResource is closed"); + }; + + private static final XAResource CLOSED_XA_RESOURCE = + (XAResource) newProxyInstance(XAResource.class.getClassLoader(), new Class[]{XAResource.class}, CLOSED_HANDLER); + + private final ConnectionHandler handler; + private XAResource wrappedXAResource; + + public XAResourceWrapper(ConnectionHandler connectionHandler, XAResource resource, AutoCloseableElement head) { + super(head); + handler = connectionHandler; + wrappedXAResource = resource; + } + + @Override + public boolean isClosed() throws Exception { + return wrappedXAResource == CLOSED_XA_RESOURCE; + } + + @Override + public void close() throws SQLException { + handler.traceConnectionOperation("xaResource.close()"); + if (wrappedXAResource != CLOSED_XA_RESOURCE) { + wrappedXAResource = CLOSED_XA_RESOURCE; + } + } + + @Override + public void commit(Xid xid, boolean onePhase) throws XAException { + handler.traceConnectionOperation("xaResource.commit()"); + wrappedXAResource.commit(xid, onePhase); + } + + @Override + public void end(Xid xid, int flags) throws XAException { + handler.traceConnectionOperation("xaResource.end()"); + wrappedXAResource.end(xid, flags); + } + + @Override + public void forget(Xid xid) throws XAException { + handler.traceConnectionOperation("xaResource.forget()"); + wrappedXAResource.forget(xid); + } + + @Override + public int getTransactionTimeout() throws XAException { + handler.traceConnectionOperation("xaResource.getTransactionTimeout()"); + return wrappedXAResource.getTransactionTimeout(); + } + + @Override + public boolean isSameRM(XAResource xares) throws XAException { + handler.traceConnectionOperation("xaResource.isSameRM()"); + return wrappedXAResource.isSameRM(xares); + } + + @Override + public int prepare(Xid xid) throws XAException { + handler.traceConnectionOperation("xaResource.prepare()"); + return wrappedXAResource.prepare(xid); + } + + @Override + public Xid[] recover(int flag) throws XAException { + handler.traceConnectionOperation("xaResource.recover()"); + return wrappedXAResource.recover(flag); + } + + @Override + public void rollback(Xid xid) throws XAException { + handler.traceConnectionOperation("xaResource.rollback()"); + wrappedXAResource.rollback(xid); + } + + @Override + public boolean setTransactionTimeout(int seconds) throws XAException { + handler.traceConnectionOperation("xaResource.setTransactionTimeout()"); + return wrappedXAResource.setTransactionTimeout(seconds); + } + + @Override + public void start(Xid xid, int flags) throws XAException { + handler.traceConnectionOperation("xaResource.start()"); + wrappedXAResource.start(xid, flags); + } +} diff --git a/jdbc-query/build.gradle b/jdbc-query/build.gradle index 1377c3f..4c6f2ac 100644 --- a/jdbc-query/build.gradle +++ b/jdbc-query/build.gradle @@ -1,5 +1,4 @@ dependencies { - api project(':jdbc-connection-pool') testImplementation project(':jdbc-test') testImplementation testLibs.derby testImplementation testLibs.hsqldb diff --git a/jdbc-query/src/main/java/module-info.java b/jdbc-query/src/main/java/module-info.java index 935bbe0..7457cde 100644 --- a/jdbc-query/src/main/java/module-info.java +++ b/jdbc-query/src/main/java/module-info.java @@ -5,7 +5,6 @@ import org.xbib.jdbc.query.flavor.Hsql; module org.xbib.jdbc.query { uses Flavor; - requires org.xbib.jdbc.connection.pool; requires java.sql; exports org.xbib.jdbc.query; provides Flavor with Derby, Hsql, H2; diff --git a/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java index e9afa07..4887719 100644 --- a/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java +++ b/jdbc-query/src/main/java/org/xbib/jdbc/query/DatabaseProvider.java @@ -1,7 +1,5 @@ package org.xbib.jdbc.query; -import org.xbib.jdbc.connection.pool.PoolConfig; -import org.xbib.jdbc.connection.pool.PoolDataSource; import org.xbib.jdbc.query.util.Metric; import javax.sql.DataSource; @@ -9,7 +7,6 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -24,8 +21,6 @@ public final class DatabaseProvider implements Supplier { private static final Logger logger = Logger.getLogger(DatabaseProvider.class.getName()); - private static final AtomicInteger poolNameCounter = new AtomicInteger(1); - private final DatabaseProviderBuilderImpl builder; private Connection connection; @@ -53,36 +48,6 @@ public final class DatabaseProvider implements Supplier { }, new OptionsDefault(Flavor.valueOf(flavor))); } - /** - * Configure the database from the following properties read from the provided configuration: - *

-     *   database.url=...       Database connect string (required)
-     *   database.user=...      Authenticate as this user (optional if provided in url)
-     *   database.password=...  User password (optional if user and password provided in
-     *                          url; prompted on standard input if user is provided and
-     *                          password is not)
-     *   database.pool.size=... How many connections in the connection pool (default 10).
-     *   database.driver.class  The driver to initialize with Class.forName(). This will
-     *                          be guessed from the database.url if not provided.
-     *   database.flavor        A {@link Flavor}. If this is not provided the flavor will be guessed based on the
-     *                          value for database.url, if possible.
-     * 
- * - *

The database flavor will be guessed based on the URL.

- * - *

A database pool will be created using jdbc-connection-pool.

- */ - public static DatabaseProviderBuilder builder(Config config) { - String flavorString = config.getString("database.flavor"); - if (flavorString == null) { - String url = config.getString("database.url"); - if (url == null) { - throw new DatabaseException("You must provide database.url"); - } - flavorString = Flavor.fromJdbcUrl(url).getName(); - } - return builder(createDataSource(config), flavorString); - } /** * Builder method to create and initialize an instance of this class using @@ -137,31 +102,6 @@ public final class DatabaseProvider implements Supplier { }, new OptionsDefault(Flavor.fromJdbcUrl(url))); } - private static DataSource createDataSource(Config config) { - String url = config.getString("database.url"); - if (url == null) { - throw new DatabaseException("You must provide database.url"); - } - PoolConfig poolConfig = new PoolConfig(); - poolConfig.setPoolName(config.getString("database.pool.name", "database-pool-" + poolNameCounter.getAndAdd(1))); - poolConfig.setUrl(config.getString("database.url")); - // provide driver class only if in config - String driverClassName = config.getString("database.driver.class"); - if (driverClassName != null) { - poolConfig.setDriverClassName(config.getString("database.driver.class")); - } - poolConfig.setUsername(config.getString("database.user")); - poolConfig.setPassword(config.getString("database.password")); - poolConfig.setMaximumPoolSize(config.getInteger("database.pool.size", 8)); - poolConfig.setAutoCommit(false); - try { - return new PoolDataSource(poolConfig); - } catch (Exception e) { - logger.log(Level.SEVERE, e.getMessage(), e); - return null; - } - } - public void transact(final DbCode code) { boolean complete = false; try { diff --git a/jdbc-test/build.gradle b/jdbc-test/build.gradle index 6b790df..78f16f4 100644 --- a/jdbc-test/build.gradle +++ b/jdbc-test/build.gradle @@ -1,5 +1,6 @@ dependencies { api project(":jdbc-query") + implementation project(":jdbc-connection-pool") implementation testLibs.junit.jupiter.api implementation testLibs.hamcrest }