This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit 4a3aadd12563186a1262c299a4cde197127d68ea Author: Andrus Adamchik <[email protected]> AuthorDate: Tue Jul 21 15:41:31 2026 +0200 CAY-2983 CayenneDataSource: Public-facing DataSource builder --- README.md | 7 +- RELEASE-NOTES.txt | 1 + .../org/apache/cayenne/tools/DbGenerateTask.java | 6 +- .../configuration/DefaultRuntimeProperties.java | 13 + .../cayenne/configuration/RuntimeProperties.java | 11 +- .../runtime/PropertyDataSourceFactory.java | 69 +- .../runtime/XMLPoolingDataSourceFactory.java | 13 +- .../cayenne/datasource/CayenneDataSource.java | 301 ++++++ .../cayenne/datasource/DataSourceBuilder.java | 4 +- .../cayenne/datasource/DriverDataSource.java | 262 +++-- .../datasource/ManagedPoolingDataSource.java | 11 +- .../cayenne/datasource/PoolAwareConnection.java | 1071 ++++++++++---------- .../cayenne/datasource/PoolingDataSource.java | 3 +- .../datasource/PoolingDataSourceBuilder.java | 8 +- .../cayenne/runtime/CayenneRuntimeBuilder.java | 4 +- .../cayenne/datasource/CayenneDataSourceTest.java | 196 ++++ .../org/apache/cayenne/unit/CayenneTestsEnv.java | 8 +- .../_getting-started-db-first/part4-java-code.adoc | 5 +- 18 files changed, 1223 insertions(+), 770 deletions(-) diff --git a/README.md b/README.md index 6f57faa09..a0a4eec38 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ Maven <dependency> <groupId>org.apache.cayenne</groupId> <artifactId>cayenne-server</artifactId> - <version>5.0-M1</version> + <version>5.0-M2</version> </dependency> </dependencies> ``` @@ -69,9 +69,8 @@ compile cayenne.dependency('server') ```java CayenneRuntime cayenneRuntime = CayenneRuntime.builder() .addConfig("cayenne-demo.xml") - .dataSource(DataSourceBuilder - .url("jdbc:mysql://localhost:3306/cayenne_demo") - .driver("com.mysql.cj.jdbc.Driver") + .dataSource(CayenneDataSource + .of("jdbc:mysql://localhost:3306/cayenne_demo") .userName("username") .password("password") .build()) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 0361549fa..17bd960ef 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -29,6 +29,7 @@ CAY-2978 AI skill: "cayenne-model-naming" CAY-2979 AI skill: "cayenne-full-db-sync" CAY-2980 Improve model name generation CAY-2982 Modeler: when upgrading projects allow upgraders to send outcome messages +CAY-2983 CayenneDataSource: Public-facing DataSource builder Bug Fixes: diff --git a/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/DbGenerateTask.java b/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/DbGenerateTask.java index 5306bdeb5..8100fff88 100644 --- a/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/DbGenerateTask.java +++ b/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/DbGenerateTask.java @@ -26,7 +26,7 @@ import org.apache.cayenne.configuration.DataNodeDescriptor; import org.apache.cayenne.configuration.runtime.DataSourceFactory; import org.apache.cayenne.configuration.runtime.DbAdapterFactory; import org.apache.cayenne.configuration.runtime.PkGeneratorFactoryProvider; -import org.apache.cayenne.datasource.DataSourceBuilder; +import org.apache.cayenne.datasource.CayenneDataSource; import org.apache.cayenne.dba.DbAdapter; import org.apache.cayenne.dba.JdbcAdapter; import org.apache.cayenne.dba.PkGenerator; @@ -165,8 +165,8 @@ public class DbGenerateTask extends BaseCayenneTask { } DataSource createDataSource() { - return DataSourceBuilder.url(dataSource.getUrl()) - .driver(dataSource.getDriver()) + return CayenneDataSource.of(dataSource.getUrl()) + .driverClass(dataSource.getDriver()) .userName(dataSource.getUsername()) .password(dataSource.getPassword()) .build(); diff --git a/cayenne/src/main/java/org/apache/cayenne/configuration/DefaultRuntimeProperties.java b/cayenne/src/main/java/org/apache/cayenne/configuration/DefaultRuntimeProperties.java index 62daa73c4..2edfdf6be 100644 --- a/cayenne/src/main/java/org/apache/cayenne/configuration/DefaultRuntimeProperties.java +++ b/cayenne/src/main/java/org/apache/cayenne/configuration/DefaultRuntimeProperties.java @@ -18,6 +18,7 @@ ****************************************************************/ package org.apache.cayenne.configuration; +import java.util.HashMap; import java.util.Map; import org.apache.cayenne.di.Inject; @@ -98,4 +99,16 @@ public class DefaultRuntimeProperties implements RuntimeProperties { String string = get(key); return string != null ? "true".equalsIgnoreCase(string) : defaultValue; } + + @Override + public Map<String, String> toMap() { + Map<String, String> map = new HashMap<>(properties); + + // system properties take precedence, matching the "get" behavior + for (String name : System.getProperties().stringPropertyNames()) { + map.put(name, System.getProperty(name)); + } + + return map; + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/configuration/RuntimeProperties.java b/cayenne/src/main/java/org/apache/cayenne/configuration/RuntimeProperties.java index dba18b255..363f5649e 100644 --- a/cayenne/src/main/java/org/apache/cayenne/configuration/RuntimeProperties.java +++ b/cayenne/src/main/java/org/apache/cayenne/configuration/RuntimeProperties.java @@ -18,9 +18,11 @@ ****************************************************************/ package org.apache.cayenne.configuration; +import java.util.Map; + /** * Represents a properties map for a given {@link org.apache.cayenne.runtime.CayenneRuntime}. - * + * * @since 3.1 */ public interface RuntimeProperties { @@ -43,4 +45,11 @@ public interface RuntimeProperties { long getLong(String key, long defaultValue); boolean getBoolean(String key, boolean defaultValue); + + /** + * Returns a snapshot of the properties as a map, with the same key resolution rules as {@link #get(String)}. + * + * @since 5.0 + */ + Map<String, String> toMap(); } diff --git a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/PropertyDataSourceFactory.java b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/PropertyDataSourceFactory.java index 622dd1ed6..aced37b81 100644 --- a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/PropertyDataSourceFactory.java +++ b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/PropertyDataSourceFactory.java @@ -18,32 +18,27 @@ ****************************************************************/ package org.apache.cayenne.configuration.runtime; -import org.apache.cayenne.ConfigurationException; -import org.apache.cayenne.configuration.Constants; import org.apache.cayenne.configuration.DataNodeDescriptor; import org.apache.cayenne.configuration.RuntimeProperties; -import org.apache.cayenne.datasource.DataSourceBuilder; -import org.apache.cayenne.datasource.UnmanagedPoolingDataSource; -import org.apache.cayenne.di.AdhocObjectFactory; import org.apache.cayenne.di.Inject; +import org.apache.cayenne.datasource.CayenneDataSource; import javax.sql.DataSource; -import java.sql.Driver; /** - * A DataSourceFactrory that creates a DataSource based on system properties. - * Properties can be set per domain/node name or globally, applying to all nodes - * without explicit property set. The following properties are supported: + * A DataSourceFactory that creates a DataSource based on runtime properties. Properties can be set per domain/node + * name or globally, applying to all nodes without explicit property set. The following properties are supported: * <ul> - * <li>cayenne.jdbc.driver[.domain_name.node_name] * <li>cayenne.jdbc.url[.domain_name.node_name] + * <li>cayenne.jdbc.driver[.domain_name.node_name] * <li>cayenne.jdbc.username[.domain_name.node_name] * <li>cayenne.jdbc.password[.domain_name.node_name] - * <li>cayenne.jdbc.min.connections[.domain_name.node_name] - * <li>cayenne.jdbc.max.conections[.domain_name.node_name] + * <li>cayenne.jdbc.min_connections[.domain_name.node_name] + * <li>cayenne.jdbc.max_connections[.domain_name.node_name] + * <li>cayenne.jdbc.max_wait[.domain_name.node_name] + * <li>cayenne.jdbc.validation_query[.domain_name.node_name] * </ul> - * At least url and driver properties must be specified for this factory to - * return a valid DataSource. + * The URL property is required. Pooling is enabled if at least one of the connections count properties is set. * * @since 3.1 */ @@ -52,51 +47,9 @@ public class PropertyDataSourceFactory implements DataSourceFactory { @Inject protected RuntimeProperties properties; - @Inject - private AdhocObjectFactory objectFactory; - @Override public DataSource getDataSource(DataNodeDescriptor nodeDescriptor) { - - String suffix = "." + nodeDescriptor.getDataChannelDescriptor().getName() + "." + nodeDescriptor.getName(); - - String driverClass = getProperty(Constants.JDBC_DRIVER_PROPERTY, suffix); - String url = getProperty(Constants.JDBC_URL_PROPERTY, suffix); - String username = getProperty(Constants.JDBC_USERNAME_PROPERTY, suffix); - String password = getProperty(Constants.JDBC_PASSWORD_PROPERTY, suffix); - int minConnections = getIntProperty(Constants.JDBC_MIN_CONNECTIONS_PROPERTY, suffix, 1); - int maxConnections = getIntProperty(Constants.JDBC_MAX_CONNECTIONS_PROPERTY, suffix, 1); - long maxQueueWaitTime = properties.getLong(Constants.JDBC_MAX_QUEUE_WAIT_TIME, - UnmanagedPoolingDataSource.MAX_QUEUE_WAIT_DEFAULT); - String validationQuery = properties.get(Constants.JDBC_VALIDATION_QUERY_PROPERTY); - - Driver driver = objectFactory.newInstance(Driver.class, driverClass, true); - return DataSourceBuilder - .url(url) - .driver(driver) - .userName(username) - .password(password) - .pool(minConnections, maxConnections) - .maxQueueWaitTime(maxQueueWaitTime) - .validationQuery(validationQuery).build(); - } - - protected int getIntProperty(String propertyName, String suffix, int defaultValue) { - String string = getProperty(propertyName, suffix); - - if (string == null) { - return defaultValue; - } - - try { - return Integer.parseInt(string); - } catch (NumberFormatException e) { - throw new ConfigurationException("Invalid int property '%s': '%s'", propertyName, string); - } - } - - protected String getProperty(String propertyName, String suffix) { - String value = properties.get(propertyName + suffix); - return value != null ? value : properties.get(propertyName); + String nodeName = nodeDescriptor.getDataChannelDescriptor().getName() + "." + nodeDescriptor.getName(); + return CayenneDataSource.fromProperties(properties.toMap(), nodeName).build(); } } diff --git a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/XMLPoolingDataSourceFactory.java b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/XMLPoolingDataSourceFactory.java index 7af3c97e5..9ef95f18c 100644 --- a/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/XMLPoolingDataSourceFactory.java +++ b/cayenne/src/main/java/org/apache/cayenne/configuration/runtime/XMLPoolingDataSourceFactory.java @@ -23,15 +23,13 @@ import org.apache.cayenne.configuration.Constants; import org.apache.cayenne.configuration.DataNodeDescriptor; import org.apache.cayenne.configuration.DataSourceDescriptor; import org.apache.cayenne.configuration.RuntimeProperties; -import org.apache.cayenne.datasource.DataSourceBuilder; import org.apache.cayenne.datasource.UnmanagedPoolingDataSource; -import org.apache.cayenne.di.AdhocObjectFactory; import org.apache.cayenne.di.Inject; +import org.apache.cayenne.datasource.CayenneDataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sql.DataSource; -import java.sql.Driver; /** * A {@link DataSourceFactory} that loads JDBC connection information from an @@ -48,9 +46,6 @@ public class XMLPoolingDataSourceFactory implements DataSourceFactory { @Inject private RuntimeProperties properties; - @Inject - private AdhocObjectFactory objectFactory; - @Override public DataSource getDataSource(DataNodeDescriptor nodeDescriptor) { @@ -64,10 +59,8 @@ public class XMLPoolingDataSourceFactory implements DataSourceFactory { long maxQueueWaitTime = properties .getLong(Constants.JDBC_MAX_QUEUE_WAIT_TIME, UnmanagedPoolingDataSource.MAX_QUEUE_WAIT_DEFAULT); - Driver driver = objectFactory.newInstance(Driver.class, descriptor.getJdbcDriver(), true); - - return DataSourceBuilder.url(descriptor.getDataSourceUrl()) - .driver(driver) + return CayenneDataSource.of(descriptor.getDataSourceUrl()) + .driverClass(descriptor.getJdbcDriver()) .userName(descriptor.getUserName()) .password(descriptor.getPassword()) .pool(descriptor.getMinConnections(), descriptor.getMaxConnections()) diff --git a/cayenne/src/main/java/org/apache/cayenne/datasource/CayenneDataSource.java b/cayenne/src/main/java/org/apache/cayenne/datasource/CayenneDataSource.java new file mode 100644 index 000000000..1911f098d --- /dev/null +++ b/cayenne/src/main/java/org/apache/cayenne/datasource/CayenneDataSource.java @@ -0,0 +1,301 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.datasource; + +import org.apache.cayenne.CayenneRuntimeException; +import org.apache.cayenne.configuration.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.sql.Driver; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.Map; +import java.util.Objects; + +/** + * An entry point for manually building DataSources. Produces instances of Cayenne own DataSource. Alternatively, you + * can use any JDBC-compliant third-party DataSources with Cayenne. + * + * @since 5.0 + */ +public class CayenneDataSource { + + private static final Logger LOGGER = LoggerFactory.getLogger(CayenneDataSource.class); + + /** + * Starts building a DataSource for the given database URL. + */ + public static Builder of(String url) { + return new Builder(url); + } + + /** + * Starts building a DataSource with settings taken from the following map keys: + * <ul> + * <li>cayenne.jdbc.url + * <li>cayenne.jdbc.driver + * <li>cayenne.jdbc.username + * <li>cayenne.jdbc.password + * <li>cayenne.jdbc.min_connections + * <li>cayenne.jdbc.max_connections + * <li>cayenne.jdbc.max_wait + * <li>cayenne.jdbc.validation_query + * </ul> + * The URL property is required. Pooling is enabled if at least one of the connections count properties is set. + * Within the Cayenne stack, use {@code RuntimeProperties.toMap()} to pass the runtime properties here. + */ + public static Builder fromProperties(Map<String, String> properties) { + Objects.requireNonNull(properties, "Null 'properties'"); + return fromResolvedProperties(properties, ""); + } + + /** + * Starts building a DataSource with settings taken from a properties map, same as {@link #fromProperties(Map)}, + * but first checking properties with a ".nodeName" suffix (e.g. "cayenne.jdbc.url.mynode"), and falling back to + * the unsuffixed ones. + */ + public static Builder fromProperties(Map<String, String> properties, String nodeName) { + Objects.requireNonNull(properties, "Null 'properties'"); + Objects.requireNonNull(nodeName, "Null 'nodeName'"); + return fromResolvedProperties(properties, "." + nodeName); + } + + private static Builder fromResolvedProperties(Map<String, String> props, String suffix) { + + String url = prop(props, Constants.JDBC_URL_PROPERTY, suffix); + if (url == null) { + throw new CayenneRuntimeException("Missing DataSource URL property '%s%s'", + Constants.JDBC_URL_PROPERTY, + suffix); + } + + Builder builder = new Builder(url) + .userName(prop(props, Constants.JDBC_USERNAME_PROPERTY, suffix)) + .password(prop(props, Constants.JDBC_PASSWORD_PROPERTY, suffix)); + + String driverClassName = prop(props, Constants.JDBC_DRIVER_PROPERTY, suffix); + if (driverClassName != null) { + builder.driverClass(driverClassName); + } + + int minConnections = intProp(props, Constants.JDBC_MIN_CONNECTIONS_PROPERTY, suffix, -1); + int maxConnections = intProp(props, Constants.JDBC_MAX_CONNECTIONS_PROPERTY, suffix, -1); + if (minConnections >= 0 || maxConnections >= 0) { + int min = minConnections >= 0 ? minConnections : 1; + builder.pool(min, maxConnections >= 0 ? maxConnections : Math.max(min, 1)); + } + + long maxQueueWaitTime = longProp(props, Constants.JDBC_MAX_QUEUE_WAIT_TIME, suffix, -1); + if (maxQueueWaitTime >= 0) { + builder.maxQueueWaitTime(maxQueueWaitTime); + } + + String validationQuery = prop(props, Constants.JDBC_VALIDATION_QUERY_PROPERTY, suffix); + if (validationQuery != null) { + builder.validationQuery(validationQuery); + } + + return builder; + } + + private static String prop(Map<String, String> props, String name, String suffix) { + // fallback to default property shared by all data nodes + String value = props.get(name + suffix); + return value != null ? value : props.get(name); + } + + private static int intProp(Map<String, String> props, String name, String suffix, int defaultValue) { + String value = prop(props, name, suffix); + if (value == null) { + return defaultValue; + } + + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + private static long longProp(Map<String, String> props, String name, String suffix, long defaultValue) { + String value = prop(props, name, suffix); + if (value == null) { + return defaultValue; + } + + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + private CayenneDataSource() { + } + + public static class Builder { + + private final String url; + private String userName; + private String password; + private String driverClass; + + private Integer minConnections; + private Integer maxConnections; + private Long maxQueueWaitTime; + private String validationQuery; + + private Builder(String url) { + this.url = Objects.requireNonNull(url, "Null 'url'"); + } + + public Builder userName(String userName) { + this.userName = userName; + return this; + } + + public Builder password(String password) { + this.password = password; + return this; + } + + /** + * Sets a class name of the JDBC driver. This i optional and is only used in special circumstances. Normally, + * JDBC-compliant drivers are discovered automatically, and resolved based on the URL. + */ + public Builder driverClass(String driverClassName) { + this.driverClass = driverClassName; + return this; + } + + /** + * Turns the built DataSource into a connection pool with the given connection count bounds. + */ + public Builder pool(int minConnections, int maxConnections) { + + if (minConnections < 0) { + throw new CayenneRuntimeException("Minimum number of connections can not be negative (%d)", minConnections); + } + + if (maxConnections < 0) { + throw new CayenneRuntimeException("Maximum number of connections can not be negative (%d)", maxConnections); + } + + if (minConnections > maxConnections) { + throw new CayenneRuntimeException("Minimum number of connections can not be bigger than maximum."); + } + + this.minConnections = minConnections; + this.maxConnections = maxConnections; + return this; + } + + /** + * Sets a maximum time in milliseconds a connection request may wait for a free connection in the pool. Ignored + * unless {@link #pool(int, int)} is also called. + */ + public Builder maxQueueWaitTime(long maxQueueWaitTime) { + this.maxQueueWaitTime = maxQueueWaitTime; + return this; + } + + /** + * Sets a SQL query used by the pool to check connection health. Ignored unless {@link #pool(int, int)} is also + * called. + */ + public Builder validationQuery(String validationQuery) { + this.validationQuery = validationQuery; + return this; + } + + /** + * Builds a DataSource that is pooling if {@link #pool(int, int)} was called, and non-pooling otherwise. A + * pooling DataSource implements {@link PoolingDataSource} and must be explicitly closed by the caller when no + * longer in use. + */ + public DataSource build() { + + if (minConnections == null) { + if (maxQueueWaitTime != null) { + LOGGER.warn("'maxQueueWaitTime' is ignored for a non-pooling DataSource. Call 'pool(min, max)' to enable pooling."); + } + + if (validationQuery != null) { + LOGGER.warn("'validationQuery' is ignored for a non-pooling DataSource. Call 'pool(min, max)' to enable pooling."); + } + } + + DataSource nonPooling = new DriverDataSource(loadDriver(), url, userName, password); + return minConnections != null ? pool(nonPooling) : nonPooling; + } + + private PoolingDataSource pool(DataSource nonPooling) { + + PoolingDataSourceParameters parameters = new PoolingDataSourceParameters(); + parameters.setMinConnections(minConnections); + parameters.setMaxConnections(maxConnections); + parameters.setMaxQueueWaitTime( + maxQueueWaitTime != null ? maxQueueWaitTime : UnmanagedPoolingDataSource.MAX_QUEUE_WAIT_DEFAULT); + parameters.setValidationQuery(validationQuery); + + return new ManagedPoolingDataSource(new UnmanagedPoolingDataSource(nonPooling, parameters)); + } + + private Driver loadDriver() { + + if (driverClass == null) { + try { + return DriverManager.getDriver(url); + } catch (SQLException ex) { + throw new CayenneRuntimeException("No registered JDBC driver accepting the URL '%s': %s", + url, + ex.getMessage()); + } + } + + return DriverManager.drivers() + .filter(d -> d.getClass().getName().equals(driverClass)) + .findFirst() + .orElseGet(this::instantiateDriver); + } + + private Driver instantiateDriver() { + + Class<?> driverClass; + try { + // note: implicitly using current class's ClassLoader .... + driverClass = Class.forName(this.driverClass); + } catch (Exception ex) { + throw new CayenneRuntimeException("Can not load JDBC driver named '%s': %s", + this.driverClass, + ex.getMessage()); + } + + try { + return (Driver) driverClass.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + throw new CayenneRuntimeException("Error instantiating driver '%s': %s", + this.driverClass, + ex.getMessage()); + } + } + } +} diff --git a/cayenne/src/main/java/org/apache/cayenne/datasource/DataSourceBuilder.java b/cayenne/src/main/java/org/apache/cayenne/datasource/DataSourceBuilder.java index 77c3731d4..b00724cab 100644 --- a/cayenne/src/main/java/org/apache/cayenne/datasource/DataSourceBuilder.java +++ b/cayenne/src/main/java/org/apache/cayenne/datasource/DataSourceBuilder.java @@ -26,9 +26,11 @@ import java.sql.Driver; /** * A builder class that allows to build a {@link DataSource} with optional * pooling. - * + * * @since 4.0 + * @deprecated in favor of {@link CayenneDataSource} */ +@Deprecated(since = "5.0", forRemoval = true) public class DataSourceBuilder { private String userName; diff --git a/cayenne/src/main/java/org/apache/cayenne/datasource/DriverDataSource.java b/cayenne/src/main/java/org/apache/cayenne/datasource/DriverDataSource.java index be9fb125f..335c9fa9c 100644 --- a/cayenne/src/main/java/org/apache/cayenne/datasource/DriverDataSource.java +++ b/cayenne/src/main/java/org/apache/cayenne/datasource/DriverDataSource.java @@ -19,151 +19,143 @@ package org.apache.cayenne.datasource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; import java.io.PrintWriter; import java.sql.Connection; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; +import java.util.Objects; import java.util.Properties; -import javax.sql.DataSource; - -import org.apache.cayenne.CayenneRuntimeException; -import org.apache.cayenne.util.Util; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * A non-pooling DataSource implementation wrapping a JDBC driver. */ public class DriverDataSource implements DataSource { - private static final Logger LOGGER = LoggerFactory.getLogger(DriverDataSource.class); - - protected Driver driver; - protected String connectionUrl; - protected String userName; - protected String password; - - /** - * Creates a DriverDataSource wrapping a given Driver. If "driver" is null, - * DriverDataSource will consult DriverManager for a registered driver for - * the given URL. So when specifying null, a user must take care of - * registering the driver. "connectionUrl" on the other hand must NOT be - * null. - * - * @since 1.1 - */ - public DriverDataSource(Driver driver, String connectionUrl, String userName, String password) { - - if (connectionUrl == null) { - throw new NullPointerException("Null 'connectionUrl'"); - } - - this.driver = driver; - this.connectionUrl = connectionUrl; - this.userName = userName; - this.password = password; - } - - /** - * Returns a new database connection, using preconfigured data to locate the - * database and obtain a connection. - */ - @Override - public Connection getConnection() throws SQLException { - // login with internal credentials - return getConnection(userName, password); - } - - /** - * Returns a new database connection using provided credentials to login to - * the database. - */ - @Override - public Connection getConnection(String userName, String password) throws SQLException { - try { - - logConnect(connectionUrl, userName, password); - Connection c = null; - - if (driver == null) { - c = DriverManager.getConnection(connectionUrl, userName, password); - } else { - Properties connectProperties = new Properties(); - - if (userName != null) { - connectProperties.put("user", userName); - } - - if (password != null) { - connectProperties.put("password", password); - } - c = driver.connect(connectionUrl, connectProperties); - } - - // some drivers (Oracle) return null connections instead of throwing - // an exception... fix it here - - if (c == null) { - throw new SQLException("Can't establish connection: " + connectionUrl); - } - - LOGGER.info("+++ Connecting: SUCCESS."); - - return c; - } catch (SQLException ex) { - LOGGER.info("*** Connecting: FAILURE.", ex); - throw ex; - } - } - - private void logConnect(String url, String userName, String password) { - LOGGER.info("Connecting to '{}' as '{}'", url, userName); - } - - @Override - public int getLoginTimeout() throws SQLException { - return -1; - } - - @Override - public void setLoginTimeout(int seconds) throws SQLException { - // noop - } - - @Override - public PrintWriter getLogWriter() throws SQLException { - return DriverManager.getLogWriter(); - } - - @Override - public void setLogWriter(PrintWriter out) throws SQLException { - DriverManager.setLogWriter(out); - } - - /** - * @since 3.0 - */ - @Override - public boolean isWrapperFor(Class<?> iface) throws SQLException { - throw new UnsupportedOperationException(); - } - - /** - * @since 3.0 - */ - @Override - public <T> T unwrap(Class<T> iface) throws SQLException { - throw new UnsupportedOperationException(); - } - - /** - * @since 3.1 - */ - @Override - public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException { - throw new UnsupportedOperationException(); - } + private static final Logger LOGGER = LoggerFactory.getLogger(DriverDataSource.class); + + protected final Driver driver; + protected final String connectionUrl; + protected final String userName; + protected final String password; + + /** + * Creates a DriverDataSource wrapping a given Driver. If "driver" is null, + * DriverDataSource will consult DriverManager for a registered driver for + * the given URL. So when specifying null, a user must take care of + * registering the driver. "connectionUrl" on the other hand must NOT be + * null. + * + * @since 1.1 + */ + public DriverDataSource(Driver driver, String connectionUrl, String userName, String password) { + this.driver = driver; + this.connectionUrl = Objects.requireNonNull(connectionUrl, "Null 'connectionUrl'"); + this.userName = userName; + this.password = password; + } + + /** + * Returns a new database connection, using preconfigured data to locate the + * database and obtain a connection. + */ + @Override + public Connection getConnection() throws SQLException { + // login with internal credentials + return getConnection(userName, password); + } + + /** + * Returns a new database connection using provided credentials to login to + * the database. + */ + @Override + public Connection getConnection(String userName, String password) throws SQLException { + try { + + logConnect(connectionUrl, userName); + Connection c; + + if (driver == null) { + c = DriverManager.getConnection(connectionUrl, userName, password); + } else { + Properties connectProperties = new Properties(); + + if (userName != null) { + connectProperties.put("user", userName); + } + + if (password != null) { + connectProperties.put("password", password); + } + c = driver.connect(connectionUrl, connectProperties); + } + + // some drivers (Oracle) return null connections instead of throwing + // an exception... fix it here + + if (c == null) { + throw new SQLException("Can't establish connection: " + connectionUrl); + } + + LOGGER.info("+++ Connecting: SUCCESS."); + + return c; + } catch (SQLException ex) { + LOGGER.info("*** Connecting: FAILURE.", ex); + throw ex; + } + } + + private void logConnect(String url, String userName) { + LOGGER.info("Connecting to '{}' as '{}'", url, userName); + } + + @Override + public int getLoginTimeout() { + return -1; + } + + @Override + public void setLoginTimeout(int seconds) { + // noop + } + + @Override + public PrintWriter getLogWriter() { + return DriverManager.getLogWriter(); + } + + @Override + public void setLogWriter(PrintWriter out) { + DriverManager.setLogWriter(out); + } + + /** + * @since 3.0 + */ + @Override + public boolean isWrapperFor(Class<?> iface) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * @since 3.0 + */ + @Override + public <T> T unwrap(Class<T> iface) throws SQLException { + throw new UnsupportedOperationException(); + } + + /** + * @since 3.1 + */ + @Override + public java.util.logging.Logger getParentLogger() { + throw new UnsupportedOperationException(); + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java b/cayenne/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java index 800819dc9..6237fcbb7 100644 --- a/cayenne/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java +++ b/cayenne/src/main/java/org/apache/cayenne/datasource/ManagedPoolingDataSource.java @@ -18,16 +18,15 @@ ****************************************************************/ package org.apache.cayenne.datasource; +import org.apache.cayenne.di.ScopeEventListener; + +import javax.sql.DataSource; import java.io.PrintWriter; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLFeatureNotSupportedException; import java.util.logging.Logger; -import javax.sql.DataSource; - -import org.apache.cayenne.di.ScopeEventListener; - /** * A wrapper for {@link UnmanagedPoolingDataSource} that automatically manages * the underlying connection pool size. @@ -58,10 +57,6 @@ public class ManagedPoolingDataSource implements PoolingDataSource, ScopeEventLi int poolSize() { return dataSourceManager.getDataSource().poolSize(); } - - int availableSize() { - return dataSourceManager.getDataSource().availableSize(); - } int canExpandSize() { return dataSourceManager.getDataSource().canExpandSize(); diff --git a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolAwareConnection.java b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolAwareConnection.java index 388d50298..42966f110 100644 --- a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolAwareConnection.java +++ b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolAwareConnection.java @@ -39,546 +39,545 @@ import java.util.Properties; import java.util.concurrent.Executor; /** - * A {@link Connection} wrapper that interacts with the - * {@link UnmanagedPoolingDataSource}, allowing to recycle connections and track + * A Connection wrapper that interacts with the UnmanagedPoolingDataSource, allowing to recycle connections and track * failures. - * + * * @since 4.0 */ public class PoolAwareConnection implements Connection { - private UnmanagedPoolingDataSource parent; - private Connection connection; - private String validationQuery; - - public PoolAwareConnection(UnmanagedPoolingDataSource parent, Connection connection, String validationQuery) { - this.parent = parent; - this.connection = connection; - this.validationQuery = validationQuery; - } - - Connection getConnection() { - return connection; - } - - boolean validate() { - - if (validationQuery == null) { - return true; - } - - try { - - try (Statement statement = connection.createStatement();) { - - try (ResultSet rs = statement.executeQuery(validationQuery);) { - - if (!rs.next()) { - throw new SQLException("Connection validation failed, no result for query: " + validationQuery); - } - } - } - } catch (SQLException e) { - return false; - } - - return true; - } - - void recover(SQLException reconnectCause) throws SQLException { - - try { - connection.close(); - } catch (SQLException e) { - // ignore exception, since connection is expected to be in a bad - // state - } - - // TODO: autocommit, tx isolation, and other connection settings may - // change when resetting connection and need to be restored... - try { - connection = parent.createUnwrapped(); - } catch (SQLException e) { - parent.retire(this); - throw reconnectCause; - } - } - - @Override - public void clearWarnings() throws SQLException { - try { - connection.clearWarnings(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public void close() throws SQLException { - parent.reclaim(this); - } - - @Override - public void commit() throws SQLException { - try { - connection.commit(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public Statement createStatement() throws SQLException { - try { - return connection.createStatement(); - } catch (SQLException sqlEx) { - recover(sqlEx); - return connection.createStatement(); - } - } - - @Override - public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { - try { - return connection.createStatement(resultSetType, resultSetConcurrency); - } catch (SQLException e) { - recover(e); - return connection.createStatement(resultSetType, resultSetConcurrency); - } - } - - @Override - public boolean getAutoCommit() throws SQLException { - try { - return connection.getAutoCommit(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public String getCatalog() throws SQLException { - try { - return connection.getCatalog(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public DatabaseMetaData getMetaData() throws SQLException { - try { - return connection.getMetaData(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public int getTransactionIsolation() throws SQLException { - try { - return connection.getTransactionIsolation(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public SQLWarning getWarnings() throws SQLException { - try { - return connection.getWarnings(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public boolean isClosed() throws SQLException { - - try { - return connection.isClosed(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public boolean isReadOnly() throws SQLException { - try { - return connection.isReadOnly(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public String nativeSQL(String sql) throws SQLException { - try { - return connection.nativeSQL(sql); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public CallableStatement prepareCall(String sql) throws SQLException { - try { - return connection.prepareCall(sql); - } catch (SQLException sqlEx) { - recover(sqlEx); - return connection.prepareCall(sql); - } - } - - @Override - public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { - try { - return connection.prepareCall(sql, resultSetType, resultSetConcurrency); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.prepareCall(sql, resultSetType, resultSetConcurrency); - } - } - - @Override - public PreparedStatement prepareStatement(String sql) throws SQLException { - try { - return connection.prepareStatement(sql); - } catch (SQLException sqlEx) { - recover(sqlEx); - return connection.prepareStatement(sql); - } - } - - @Override - public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) - throws SQLException { - try { - return connection.prepareStatement(sql, resultSetType, resultSetConcurrency); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.prepareStatement(sql, resultSetType, resultSetConcurrency); - } - } - - @Override - public void rollback() throws SQLException { - try { - connection.rollback(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public void setAutoCommit(boolean autoCommit) throws SQLException { - try { - connection.setAutoCommit(autoCommit); - } catch (SQLException sqlEx) { - - try { - UnmanagedPoolingDataSource.sybaseAutoCommitPatch(connection, sqlEx, autoCommit); - } catch (SQLException patchEx) { - parent.retire(this); - throw sqlEx; - } - } - } - - @Override - public void setCatalog(String catalog) throws SQLException { - try { - connection.setCatalog(catalog); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public void setReadOnly(boolean readOnly) throws SQLException { - try { - connection.setReadOnly(readOnly); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public void setTransactionIsolation(int level) throws SQLException { - try { - connection.setTransactionIsolation(level); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public Map<String, Class<?>> getTypeMap() throws SQLException { - try { - return connection.getTypeMap(); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public void setTypeMap(Map<String, Class<?>> map) throws SQLException { - try { - connection.setTypeMap(map); - } catch (SQLException sqlEx) { - parent.retire(this); - throw sqlEx; - } - } - - @Override - public void setHoldability(int holdability) throws SQLException { - throw new java.lang.UnsupportedOperationException("Method setHoldability() not yet implemented."); - } - - @Override - public int getHoldability() throws SQLException { - throw new java.lang.UnsupportedOperationException("Method getHoldability() not yet implemented."); - } - - @Override - public Savepoint setSavepoint() throws SQLException { - throw new java.lang.UnsupportedOperationException("Method setSavepoint() not yet implemented."); - } - - @Override - public Savepoint setSavepoint(String name) throws SQLException { - throw new java.lang.UnsupportedOperationException("Method setSavepoint() not yet implemented."); - } - - @Override - public void rollback(Savepoint savepoint) throws SQLException { - throw new java.lang.UnsupportedOperationException("Method rollback() not yet implemented."); - } - - @Override - public void releaseSavepoint(Savepoint savepoint) throws SQLException { - throw new java.lang.UnsupportedOperationException("Method releaseSavepoint() not yet implemented."); - } - - @Override - public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) - throws SQLException { - throw new java.lang.UnsupportedOperationException("Method createStatement() not yet implemented."); - } - - @Override - public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, - int resultSetHoldability) throws SQLException { - throw new java.lang.UnsupportedOperationException("Method prepareStatement() not yet implemented."); - } - - @Override - public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, - int resultSetHoldability) throws SQLException { - try { - return connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); - } catch (SQLException e) { - - recover(e); - return connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); - } - } - - @Override - public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { - - try { - return connection.prepareStatement(sql, autoGeneratedKeys); - } catch (SQLException e) { - - recover(e); - return connection.prepareStatement(sql, autoGeneratedKeys); - } - } - - @Override - public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { - try { - return connection.prepareStatement(sql, columnIndexes); - } catch (SQLException e) { - - recover(e); - return connection.prepareStatement(sql, columnIndexes); - } - } - - @Override - public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { - try { - return connection.prepareStatement(sql, columnNames); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.prepareStatement(sql, columnNames); - } - } - - @Override - public Array createArrayOf(String typeName, Object[] elements) throws SQLException { - try { - return connection.createArrayOf(typeName, elements); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.createArrayOf(typeName, elements); - } - } - - @Override - public Blob createBlob() throws SQLException { - try { - return connection.createBlob(); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.createBlob(); - } - } - - @Override - public Clob createClob() throws SQLException { - try { - return connection.createClob(); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.createClob(); - } - } - - @Override - public Struct createStruct(String typeName, Object[] attributes) throws SQLException { - try { - return connection.createStruct(typeName, attributes); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.createStruct(typeName, attributes); - } - } - - @Override - public Properties getClientInfo() throws SQLException { - try { - return connection.getClientInfo(); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.getClientInfo(); - } - } - - @Override - public String getClientInfo(String name) throws SQLException { - try { - return connection.getClientInfo(name); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.getClientInfo(name); - } - } - - @Override - public boolean isValid(int timeout) throws SQLException { - try { - return connection.isValid(timeout); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.isValid(timeout); - } - } - - @Override - public boolean isWrapperFor(Class<?> iface) throws SQLException { - return (PoolAwareConnection.class.equals(iface)) ? true : connection.isWrapperFor(iface); - } - - @SuppressWarnings("unchecked") - @Override - public <T> T unwrap(Class<T> iface) throws SQLException { - return PoolAwareConnection.class.equals(iface) ? (T) this : connection.unwrap(iface); - } - - @Override - public NClob createNClob() throws SQLException { - try { - return connection.createNClob(); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.createNClob(); - } - } - - @Override - public SQLXML createSQLXML() throws SQLException { - try { - return connection.createSQLXML(); - } catch (SQLException sqlEx) { - - recover(sqlEx); - return connection.createSQLXML(); - } - } - - @Override - public void setClientInfo(Properties properties) throws SQLClientInfoException { - connection.setClientInfo(properties); - } - - @Override - public void setClientInfo(String name, String value) throws SQLClientInfoException { - connection.setClientInfo(name, value); - } - - @Override - public void setSchema(String schema) throws SQLException { - connection.setSchema(schema); - } - - @Override - public String getSchema() throws SQLException { - return connection.getSchema(); - } - - @Override - public void abort(Executor executor) throws SQLException { - connection.abort(executor); - } - - @Override - public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { - connection.setNetworkTimeout(executor, milliseconds); - } - - @Override - public int getNetworkTimeout() throws SQLException { - return connection.getNetworkTimeout(); - } + private final UnmanagedPoolingDataSource parent; + private Connection connection; + private final String validationQuery; + + public PoolAwareConnection(UnmanagedPoolingDataSource parent, Connection connection, String validationQuery) { + this.parent = parent; + this.connection = connection; + this.validationQuery = validationQuery; + } + + Connection getConnection() { + return connection; + } + + boolean validate() { + + if (validationQuery == null) { + return true; + } + + try { + + try (Statement statement = connection.createStatement()) { + + try (ResultSet rs = statement.executeQuery(validationQuery)) { + + if (!rs.next()) { + throw new SQLException("Connection validation failed, no result for query: " + validationQuery); + } + } + } + } catch (SQLException e) { + return false; + } + + return true; + } + + void recover(SQLException reconnectCause) throws SQLException { + + try { + connection.close(); + } catch (SQLException e) { + // ignore exception, since connection is expected to be in a bad + // state + } + + // TODO: autocommit, tx isolation, and other connection settings may + // change when resetting connection and need to be restored... + try { + connection = parent.createUnwrapped(); + } catch (SQLException e) { + parent.retire(this); + throw reconnectCause; + } + } + + @Override + public void clearWarnings() throws SQLException { + try { + connection.clearWarnings(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public void close() throws SQLException { + parent.reclaim(this); + } + + @Override + public void commit() throws SQLException { + try { + connection.commit(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public Statement createStatement() throws SQLException { + try { + return connection.createStatement(); + } catch (SQLException sqlEx) { + recover(sqlEx); + return connection.createStatement(); + } + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + try { + return connection.createStatement(resultSetType, resultSetConcurrency); + } catch (SQLException e) { + recover(e); + return connection.createStatement(resultSetType, resultSetConcurrency); + } + } + + @Override + public boolean getAutoCommit() throws SQLException { + try { + return connection.getAutoCommit(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public String getCatalog() throws SQLException { + try { + return connection.getCatalog(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + try { + return connection.getMetaData(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public int getTransactionIsolation() throws SQLException { + try { + return connection.getTransactionIsolation(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public SQLWarning getWarnings() throws SQLException { + try { + return connection.getWarnings(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public boolean isClosed() throws SQLException { + + try { + return connection.isClosed(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public boolean isReadOnly() throws SQLException { + try { + return connection.isReadOnly(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public String nativeSQL(String sql) throws SQLException { + try { + return connection.nativeSQL(sql); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + try { + return connection.prepareCall(sql); + } catch (SQLException sqlEx) { + recover(sqlEx); + return connection.prepareCall(sql); + } + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + try { + return connection.prepareCall(sql, resultSetType, resultSetConcurrency); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.prepareCall(sql, resultSetType, resultSetConcurrency); + } + } + + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + try { + return connection.prepareStatement(sql); + } catch (SQLException sqlEx) { + recover(sqlEx); + return connection.prepareStatement(sql); + } + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) + throws SQLException { + try { + return connection.prepareStatement(sql, resultSetType, resultSetConcurrency); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.prepareStatement(sql, resultSetType, resultSetConcurrency); + } + } + + @Override + public void rollback() throws SQLException { + try { + connection.rollback(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + try { + connection.setAutoCommit(autoCommit); + } catch (SQLException sqlEx) { + + try { + UnmanagedPoolingDataSource.sybaseAutoCommitPatch(connection, sqlEx, autoCommit); + } catch (SQLException patchEx) { + parent.retire(this); + throw sqlEx; + } + } + } + + @Override + public void setCatalog(String catalog) throws SQLException { + try { + connection.setCatalog(catalog); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + try { + connection.setReadOnly(readOnly); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + try { + connection.setTransactionIsolation(level); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public Map<String, Class<?>> getTypeMap() throws SQLException { + try { + return connection.getTypeMap(); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public void setTypeMap(Map<String, Class<?>> map) throws SQLException { + try { + connection.setTypeMap(map); + } catch (SQLException sqlEx) { + parent.retire(this); + throw sqlEx; + } + } + + @Override + public void setHoldability(int holdability) { + throw new java.lang.UnsupportedOperationException("Method setHoldability() not yet implemented."); + } + + @Override + public int getHoldability() { + throw new java.lang.UnsupportedOperationException("Method getHoldability() not yet implemented."); + } + + @Override + public Savepoint setSavepoint() { + throw new java.lang.UnsupportedOperationException("Method setSavepoint() not yet implemented."); + } + + @Override + public Savepoint setSavepoint(String name) { + throw new java.lang.UnsupportedOperationException("Method setSavepoint() not yet implemented."); + } + + @Override + public void rollback(Savepoint savepoint) throws SQLException { + throw new java.lang.UnsupportedOperationException("Method rollback() not yet implemented."); + } + + @Override + public void releaseSavepoint(Savepoint savepoint) { + throw new java.lang.UnsupportedOperationException("Method releaseSavepoint() not yet implemented."); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) + throws SQLException { + throw new java.lang.UnsupportedOperationException("Method createStatement() not yet implemented."); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + throw new java.lang.UnsupportedOperationException("Method prepareStatement() not yet implemented."); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, + int resultSetHoldability) throws SQLException { + try { + return connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } catch (SQLException e) { + + recover(e); + return connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); + } + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + + try { + return connection.prepareStatement(sql, autoGeneratedKeys); + } catch (SQLException e) { + + recover(e); + return connection.prepareStatement(sql, autoGeneratedKeys); + } + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + try { + return connection.prepareStatement(sql, columnIndexes); + } catch (SQLException e) { + + recover(e); + return connection.prepareStatement(sql, columnIndexes); + } + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + try { + return connection.prepareStatement(sql, columnNames); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.prepareStatement(sql, columnNames); + } + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + try { + return connection.createArrayOf(typeName, elements); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.createArrayOf(typeName, elements); + } + } + + @Override + public Blob createBlob() throws SQLException { + try { + return connection.createBlob(); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.createBlob(); + } + } + + @Override + public Clob createClob() throws SQLException { + try { + return connection.createClob(); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.createClob(); + } + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + try { + return connection.createStruct(typeName, attributes); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.createStruct(typeName, attributes); + } + } + + @Override + public Properties getClientInfo() throws SQLException { + try { + return connection.getClientInfo(); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.getClientInfo(); + } + } + + @Override + public String getClientInfo(String name) throws SQLException { + try { + return connection.getClientInfo(name); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.getClientInfo(name); + } + } + + @Override + public boolean isValid(int timeout) throws SQLException { + try { + return connection.isValid(timeout); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.isValid(timeout); + } + } + + @Override + public boolean isWrapperFor(Class<?> iface) throws SQLException { + return PoolAwareConnection.class.equals(iface) || connection.isWrapperFor(iface); + } + + @SuppressWarnings("unchecked") + @Override + public <T> T unwrap(Class<T> iface) throws SQLException { + return PoolAwareConnection.class.equals(iface) ? (T) this : connection.unwrap(iface); + } + + @Override + public NClob createNClob() throws SQLException { + try { + return connection.createNClob(); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.createNClob(); + } + } + + @Override + public SQLXML createSQLXML() throws SQLException { + try { + return connection.createSQLXML(); + } catch (SQLException sqlEx) { + + recover(sqlEx); + return connection.createSQLXML(); + } + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + connection.setClientInfo(properties); + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + connection.setClientInfo(name, value); + } + + @Override + public void setSchema(String schema) throws SQLException { + connection.setSchema(schema); + } + + @Override + public String getSchema() throws SQLException { + return connection.getSchema(); + } + + @Override + public void abort(Executor executor) throws SQLException { + connection.abort(executor); + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + connection.setNetworkTimeout(executor, milliseconds); + } + + @Override + public int getNetworkTimeout() throws SQLException { + return connection.getNetworkTimeout(); + } } diff --git a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSource.java b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSource.java index d983cc39d..d4e770595 100644 --- a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSource.java +++ b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSource.java @@ -21,8 +21,7 @@ package org.apache.cayenne.datasource; import javax.sql.DataSource; /** - * A {@link DataSource} that pools connections and requires to be explicitly - * closed. + * A {@link DataSource} that pools connections and requires to be explicitly closed. * * @since 4.0 */ diff --git a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSourceBuilder.java b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSourceBuilder.java index bd6257a3b..fd8fdd9bf 100644 --- a/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSourceBuilder.java +++ b/cayenne/src/main/java/org/apache/cayenne/datasource/PoolingDataSourceBuilder.java @@ -18,17 +18,19 @@ ****************************************************************/ package org.apache.cayenne.datasource; -import javax.sql.DataSource; - import org.apache.cayenne.CayenneRuntimeException; +import javax.sql.DataSource; + /** * Turns unpooled DataSource to a connection pool. Normally you won't be * creating this builder explicitly. Call * {@link DataSourceBuilder#pool(int, int)} method instead. - * + * * @since 4.0 + * @deprecated in favor of {@link CayenneDataSource} */ +@Deprecated(since = "5.0", forRemoval = true) public class PoolingDataSourceBuilder { private DataSourceBuilder nonPoolingBuilder; diff --git a/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntimeBuilder.java b/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntimeBuilder.java index 3e8618dc4..0a5405dc0 100644 --- a/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntimeBuilder.java +++ b/cayenne/src/main/java/org/apache/cayenne/runtime/CayenneRuntimeBuilder.java @@ -23,7 +23,7 @@ import org.apache.cayenne.configuration.Constants; import org.apache.cayenne.configuration.runtime.CoreModule; import org.apache.cayenne.configuration.runtime.DataSourceFactory; import org.apache.cayenne.configuration.runtime.CoreModuleExtender; -import org.apache.cayenne.datasource.DataSourceBuilder; +import org.apache.cayenne.datasource.CayenneDataSource; import org.apache.cayenne.di.Module; import org.apache.cayenne.di.spi.ModuleLoader; @@ -88,7 +88,7 @@ public class CayenneRuntimeBuilder { * mapping. If the mapping contains no DataNodes, and the DataSource is set * with this method, the builder would create a single default DataNode. * - * @see DataSourceBuilder + * @see CayenneDataSource */ public CayenneRuntimeBuilder dataSource(DataSource dataSource) { this.dataSourceFactory = new FixedDataSourceFactory(dataSource); diff --git a/cayenne/src/test/java/org/apache/cayenne/datasource/CayenneDataSourceTest.java b/cayenne/src/test/java/org/apache/cayenne/datasource/CayenneDataSourceTest.java new file mode 100644 index 000000000..9223c17c4 --- /dev/null +++ b/cayenne/src/test/java/org/apache/cayenne/datasource/CayenneDataSourceTest.java @@ -0,0 +1,196 @@ +/***************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + ****************************************************************/ +package org.apache.cayenne.datasource; + +import org.apache.cayenne.CayenneRuntimeException; +import org.junit.jupiter.api.Test; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +public class CayenneDataSourceTest { + + private static final String DRIVER = "org.hsqldb.jdbcDriver"; + + @Test + public void nonPoolingWhenNoPoolSettings() throws Exception { + DataSource dataSource = CayenneDataSource.of("jdbc:hsqldb:mem:cds_non_pooling") + .driverClass(DRIVER) + .userName("sa") + .password("") + .build(); + + assertInstanceOf(DriverDataSource.class, dataSource); + + try (Connection c = dataSource.getConnection()) { + assertFalse(c.isClosed()); + } + } + + @Test + public void poolingWhenPoolRequested() throws Exception { + DataSource dataSource = CayenneDataSource.of("jdbc:hsqldb:mem:cds_pooling") + .driverClass(DRIVER) + .userName("sa") + .password("") + .pool(1, 2) + .build(); + + try (PoolingDataSource pooling = assertInstanceOf(ManagedPoolingDataSource.class, dataSource); + Connection c = pooling.getConnection()) { + assertFalse(c.isClosed()); + } + } + + @Test + public void minGreaterThanMaxFails() { + CayenneDataSource.Builder builder = CayenneDataSource.of("jdbc:hsqldb:mem:cds_invalid") + .driverClass(DRIVER); + + assertThrows(CayenneRuntimeException.class, () -> builder.pool(5, 2)); + } + + @Test + public void negativeConnectionsFail() { + CayenneDataSource.Builder builder = CayenneDataSource.of("jdbc:hsqldb:mem:cds_negative") + .driverClass(DRIVER); + + assertThrows(CayenneRuntimeException.class, () -> builder.pool(-1, 2)); + } + + @Test + public void poolSettingsWithoutPoolIgnored() { + DataSource dataSource = CayenneDataSource.of("jdbc:hsqldb:mem:cds_no_pool") + .driverClass(DRIVER) + .validationQuery("SELECT 1") + .build(); + + assertInstanceOf(DriverDataSource.class, dataSource); + } + + @Test + public void driverResolvedFromUrl() throws Exception { + DataSource dataSource = CayenneDataSource.of("jdbc:hsqldb:mem:cds_url_resolved") + .userName("sa") + .password("") + .build(); + + assertInstanceOf(DriverDataSource.class, dataSource); + + try (Connection c = dataSource.getConnection()) { + assertFalse(c.isClosed()); + } + } + + @Test + public void unknownUrlWithoutDriverFails() { + CayenneDataSource.Builder builder = CayenneDataSource.of("jdbc:nosuchdb:mem:cds"); + + assertThrows(CayenneRuntimeException.class, builder::build); + } + + @Test + public void fromPropertiesNonPooling() throws Exception { + Map<String, String> properties = Map.of( + "cayenne.jdbc.url", "jdbc:hsqldb:mem:cds_props", + "cayenne.jdbc.username", "sa", + "cayenne.jdbc.password", ""); + + DataSource dataSource = CayenneDataSource.fromProperties(properties).build(); + + assertInstanceOf(DriverDataSource.class, dataSource); + + try (Connection c = dataSource.getConnection()) { + assertFalse(c.isClosed()); + } + } + + @Test + public void fromPropertiesPooling() throws Exception { + Map<String, String> properties = Map.of( + "cayenne.jdbc.url", "jdbc:hsqldb:mem:cds_props_pool", + "cayenne.jdbc.username", "sa", + "cayenne.jdbc.password", "", + "cayenne.jdbc.min_connections", "1", + "cayenne.jdbc.max_connections", "2"); + + DataSource dataSource = CayenneDataSource.fromProperties(properties).build(); + + try (PoolingDataSource pooling = assertInstanceOf(ManagedPoolingDataSource.class, dataSource); + Connection c = pooling.getConnection()) { + assertFalse(c.isClosed()); + } + } + + @Test + public void fromPropertiesWithNodeSuffixAndFallback() throws Exception { + Map<String, String> properties = Map.of( + "cayenne.jdbc.url", "jdbc:hsqldb:mem:cds_props_base", + "cayenne.jdbc.url.node1", "jdbc:hsqldb:mem:cds_props_node1", + "cayenne.jdbc.username", "sa", + "cayenne.jdbc.password", ""); + + DataSource dataSource = CayenneDataSource.fromProperties(properties, "node1").build(); + + assertInstanceOf(DriverDataSource.class, dataSource); + + try (Connection c = dataSource.getConnection()) { + assertEquals("jdbc:hsqldb:mem:cds_props_node1", c.getMetaData().getURL()); + } + } + + @Test + public void fromPropertiesMissingUrlFails() { + Map<String, String> properties = Map.of(); + + assertThrows(CayenneRuntimeException.class, () -> CayenneDataSource.fromProperties(properties)); + } + + @Test + public void fromPropertiesInvalidConnectionCountTreatedAsUnset() { + Map<String, String> properties = Map.of( + "cayenne.jdbc.url", "jdbc:hsqldb:mem:cds_props_bad_int", + "cayenne.jdbc.username", "sa", + "cayenne.jdbc.password", "", + "cayenne.jdbc.min_connections", "not_a_number"); + + DataSource dataSource = CayenneDataSource.fromProperties(properties).build(); + + assertInstanceOf(DriverDataSource.class, dataSource); + } + + @Test + public void fromPropertiesNullNodeNameFails() { + Map<String, String> properties = Map.of( + "cayenne.jdbc.url", "jdbc:hsqldb:mem:cds_props"); + + assertThrows(NullPointerException.class, () -> CayenneDataSource.fromProperties(properties, null)); + } + + @Test + public void unknownDriverFails() { + CayenneDataSource.Builder builder = CayenneDataSource.of("jdbc:example:none") + .driverClass("com.example.NoSuchDriver"); + + assertThrows(CayenneRuntimeException.class, builder::build); + } +} diff --git a/cayenne/src/test/java/org/apache/cayenne/unit/CayenneTestsEnv.java b/cayenne/src/test/java/org/apache/cayenne/unit/CayenneTestsEnv.java index 769cc58b0..0b71a3184 100644 --- a/cayenne/src/test/java/org/apache/cayenne/unit/CayenneTestsEnv.java +++ b/cayenne/src/test/java/org/apache/cayenne/unit/CayenneTestsEnv.java @@ -25,7 +25,7 @@ import org.apache.cayenne.configuration.Constants; import org.apache.cayenne.configuration.DataSourceDescriptor; import org.apache.cayenne.configuration.runtime.CoreModule; import org.apache.cayenne.configuration.runtime.DataNodeFactory; -import org.apache.cayenne.datasource.DataSourceBuilder; +import org.apache.cayenne.datasource.CayenneDataSource; import org.apache.cayenne.dba.DbAdapter; import org.apache.cayenne.dba.QuotingStrategy; import org.apache.cayenne.di.AdhocObjectFactory; @@ -64,9 +64,9 @@ public class CayenneTestsEnv implements BeforeEachCallback, AfterEachCallback { static { DataSourceDescriptor dsDescriptor = DataSourceConfigLoader.load(); - DataSource ds = DataSourceBuilder - .url(dsDescriptor.getDataSourceUrl()) - .driver(dsDescriptor.getJdbcDriver()) + DataSource ds = CayenneDataSource + .of(dsDescriptor.getDataSourceUrl()) + .driverClass(dsDescriptor.getJdbcDriver()) .userName(dsDescriptor.getUserName()) .password(dsDescriptor.getPassword()) .pool(dsDescriptor.getMinConnections(), dsDescriptor.getMaxConnections()) diff --git a/docs/asciidoc/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc b/docs/asciidoc/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc index beec80606..ad1ef0e1b 100644 --- a/docs/asciidoc/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc +++ b/docs/asciidoc/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc @@ -45,9 +45,8 @@ public class Main { public static void main(String[] args) { CayenneRuntime cayenneRuntime = CayenneRuntime.builder() - .dataSource(DataSourceBuilder - .url("jdbc:mysql://127.0.0.1:3306/cayenne_demo") - .driver("com.mysql.cj.jdbc.Driver") + .dataSource(CayenneDataSource + .of("jdbc:mysql://127.0.0.1:3306/cayenne_demo") .userName("root") // TODO: change to your actual username and password .password("your-password").build()) .addConfig("cayenne-project.xml")
