From 85f1e2c9b6a09da5ef512b4afbf906f5b4f74f8b Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 15 Mar 2021 13:59:57 -0700 Subject: [PATCH] Refactor DataSourceBuilder and add derivedFrom Refactor `DataSourceBuilder` to use direct property mappers rather than the `Binder` and aliases. Supported DataSource types now include two-way mappers which allows us to both get and set properties in a uniform way. A new `derivedFrom` factory method has been added which allows a new `DataSource` to be derived from an existing one. This update is primarily to allow Flyway and Liquibase migrations to work against a `@Bean` configured DataSource rather than assuming that the primary DataSource was always created via auto-configuration. See gh-25643 --- spring-boot-project/spring-boot/build.gradle | 6 +- .../boot/jdbc/DataSourceBuilder.java | 771 ++++++++++++++---- ...nsupportedDataSourcePropertyException.java | 40 + .../jdbc/DataSourceBuilderNoHikariTests.java | 46 ++ .../boot/jdbc/DataSourceBuilderTests.java | 195 ++++- 5 files changed, 853 insertions(+), 205 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/UnsupportedDataSourcePropertyException.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderNoHikariTests.java diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle index 259d8d4c9f..9efa1cf7ea 100644 --- a/spring-boot-project/spring-boot/build.gradle +++ b/spring-boot-project/spring-boot/build.gradle @@ -20,8 +20,10 @@ dependencies { optional("com.atomikos:transactions-jms") optional("com.atomikos:transactions-jta") optional("com.fasterxml.jackson.core:jackson-databind") + optional("com.h2database:h2") optional("com.google.code.gson:gson") optional("com.oracle.database.jdbc:ucp") + optional("com.oracle.database.jdbc:ojdbc8") optional("com.samskivert:jmustache") optional("com.zaxxer:HikariCP") optional("io.netty:netty-tcnative-boringssl-static") @@ -58,6 +60,7 @@ dependencies { optional("org.jboss:jboss-transaction-spi") optional("org.jooq:jooq") optional("org.liquibase:liquibase-core") + optional("org.postgresql:postgresql") optional("org.slf4j:jul-to-slf4j") optional("org.slf4j:slf4j-api") optional("org.springframework:spring-messaging") @@ -76,11 +79,9 @@ dependencies { testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation("com.google.appengine:appengine-api-1.0-sdk") - testImplementation("com.h2database:h2") testImplementation("com.ibm.db2:jcc") testImplementation("com.jayway.jsonpath:json-path") testImplementation("com.microsoft.sqlserver:mssql-jdbc") - testImplementation("com.oracle.database.jdbc:ojdbc8") testImplementation("com.squareup.okhttp3:okhttp") testImplementation("com.sun.xml.messaging.saaj:saaj-impl") testImplementation("io.projectreactor:reactor-test") @@ -97,7 +98,6 @@ dependencies { testImplementation("org.mariadb.jdbc:mariadb-java-client") testImplementation("org.mockito:mockito-core") testImplementation("org.mockito:mockito-junit-jupiter") - testImplementation("org.postgresql:postgresql") testImplementation("org.springframework:spring-context-support") testImplementation("org.springframework.data:spring-data-redis") testImplementation("org.springframework.data:spring-data-r2dbc") diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java index 0678d0633f..a5896e220c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/DataSourceBuilder.java @@ -16,241 +16,652 @@ package org.springframework.boot.jdbc; -import java.util.ArrayList; -import java.util.Collection; +import java.lang.reflect.Method; +import java.sql.SQLException; +import java.util.Collections; import java.util.HashMap; -import java.util.List; +import java.util.HashSet; import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Function; +import java.util.Set; +import java.util.function.Supplier; import javax.sql.DataSource; +import com.zaxxer.hikari.HikariDataSource; +import oracle.jdbc.datasource.OracleDataSource; +import oracle.ucp.jdbc.PoolDataSource; +import oracle.ucp.jdbc.PoolDataSourceImpl; +import org.apache.commons.dbcp2.BasicDataSource; +import org.h2.jdbcx.JdbcDataSource; +import org.postgresql.ds.PGSimpleDataSource; + import org.springframework.beans.BeanUtils; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.boot.context.properties.source.ConfigurationPropertyName; -import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases; -import org.springframework.boot.context.properties.source.ConfigurationPropertySource; -import org.springframework.boot.context.properties.source.MapConfigurationPropertySource; +import org.springframework.core.ResolvableType; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; /** - * Convenience class for building a {@link DataSource} with common implementations and - * properties. If HikariCP, Tomcat, Commons DBCP or Oracle UCP are on the classpath one of - * them will be selected (in that order with Hikari first). In the interest of a uniform - * interface, and so that there can be a fallback to an embedded database if one can be - * detected on the classpath, only a small set of common configuration properties are - * supported. To inject additional properties into the result you can downcast it, or use + * Convenience class for building a {@link DataSource}. Provides a limited subset of the + * properties supported by a typical {@link DataSource} as well as detection logic to pick + * the most suitable pooling {@link DataSource} implementation. + *

+ * The following pooling {@link DataSource} implementations are supported by this builder. + * When no {@link #type(Class) type} has been explicitly set, the first available pool + * implementation will be picked: + *

+ *

+ * The following non-pooling {@link DataSource} implementations can be used when + * explicitly set as a {@link #type(Class) type}: + *

+ *

+ * This class is commonly used in an {@code @Bean} method and often combined with * {@code @ConfigurationProperties}. * - * @param type of DataSource produced by the builder + * @param the {@link DataSource} type being built * @author Dave Syer * @author Madhura Bhave * @author Fabio Grassi + * @author Phillip Webb * @since 2.0.0 + * @see #create() + * @see #create(ClassLoader) + * @see #derivedFrom(DataSource) */ public final class DataSourceBuilder { - private Class type; + private final ClassLoader classLoader; - private final DataSourceSettingsResolver settingsResolver; + private final Map values = new HashMap<>(); - private final Map properties = new HashMap<>(); + private Class type; - public static DataSourceBuilder create() { - return new DataSourceBuilder<>(null); + private final T deriveFrom; + + private DataSourceBuilder(ClassLoader classLoader) { + this.classLoader = classLoader; + this.deriveFrom = null; } + @SuppressWarnings("unchecked") + private DataSourceBuilder(T deriveFrom) { + Assert.notNull(deriveFrom, "DataSource must not be null"); + this.classLoader = deriveFrom.getClass().getClassLoader(); + this.type = (Class) deriveFrom.getClass(); + this.deriveFrom = deriveFrom; + } + + /** + * Set the {@link DataSource} type that should be built. + * @param the datasource type + * @param type the datasource type + * @return this builder + */ + @SuppressWarnings("unchecked") + public DataSourceBuilder type(Class type) { + this.type = (Class) type; + return (DataSourceBuilder) this; + } + + /** + * Set the URL that should be used when building the datasource. + * @param url the JDBC url + * @return this builder + */ + public DataSourceBuilder url(String url) { + set(DataSourceProperty.URL, url); + return this; + } + + /** + * Set the driver class name that should be used when building the datasource. + * @param driverClassName the driver class name + * @return this builder + */ + public DataSourceBuilder driverClassName(String driverClassName) { + set(DataSourceProperty.DRIVER_CLASS_NAME, driverClassName); + return this; + } + + /** + * Set the username that should be used when building the datasource. + * @param username the user name + * @return this builder + */ + public DataSourceBuilder username(String username) { + set(DataSourceProperty.USERNAME, username); + return this; + } + + /** + * Set the password that should be used when building the datasource. + * @param password the password + * @return this builder + */ + public DataSourceBuilder password(String password) { + set(DataSourceProperty.PASSWORD, password); + return this; + } + + private void set(DataSourceProperty property, String value) { + this.values.put(property, value); + } + + /** + * Return a newly built {@link DataSource} instance. + * @return the built datasource + */ + public T build() { + DataSourceProperties properties = DataSourceProperties.forType(this.classLoader, this.type); + DataSourceProperties derriveFromProperties = (this.deriveFrom != null) + ? DataSourceProperties.forType(this.classLoader, this.type) : null; + Class instanceType = (this.type != null) ? this.type : properties.getDataSourceInstanceType(); + T dataSource = BeanUtils.instantiateClass(instanceType); + Set applied = new HashSet<>(); + for (DataSourceProperty property : DataSourceProperty.values()) { + if (this.values.containsKey(property)) { + String value = this.values.get(property); + properties.set(dataSource, property, value); + applied.add(property); + } + else if (derriveFromProperties != null && properties.canSet(property)) { + String value = derriveFromProperties.get(this.deriveFrom, property); + if (value != null) { + properties.set(dataSource, property, value); + applied.add(property); + } + } + } + if (!applied.contains(DataSourceProperty.DRIVER_CLASS_NAME) + && properties.canSet(DataSourceProperty.DRIVER_CLASS_NAME) + && this.values.containsKey(DataSourceProperty.URL)) { + String url = this.values.get(DataSourceProperty.URL); + DatabaseDriver driver = DatabaseDriver.fromJdbcUrl(url); + properties.set(dataSource, DataSourceProperty.DRIVER_CLASS_NAME, driver.getDriverClassName()); + } + return dataSource; + } + + /** + * Create a new {@link DataSourceBuilder} instance. + * @return a new datasource builder instance + */ + public static DataSourceBuilder create() { + return create(null); + } + + /** + * Create a new {@link DataSourceBuilder} instance. + * @param classLoader the classloader used to discover preferred settings + * @return a new {@link DataSource} builder instance + */ public static DataSourceBuilder create(ClassLoader classLoader) { return new DataSourceBuilder<>(classLoader); } - private DataSourceBuilder(ClassLoader classLoader) { - this.settingsResolver = new DataSourceSettingsResolver(classLoader); - } - - @SuppressWarnings("unchecked") - public T build() { - Class type = getType(); - DataSource result = BeanUtils.instantiateClass(type); - maybeGetDriverClassName(); - bind(result); - return (T) result; - } - - private void maybeGetDriverClassName() { - if (!this.properties.containsKey("driverClassName") && this.properties.containsKey("url")) { - String url = this.properties.get("url"); - String driverClass = DatabaseDriver.fromJdbcUrl(url).getDriverClassName(); - this.properties.put("driverClassName", driverClass); - } - } - - private void bind(DataSource result) { - ConfigurationPropertySource source = new MapConfigurationPropertySource(this.properties); - ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases(); - this.settingsResolver.registerAliases(result, aliases); - Binder binder = new Binder(source.withAliases(aliases)); - binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result)); - } - - @SuppressWarnings("unchecked") - public DataSourceBuilder type(Class type) { - this.type = type; - return (DataSourceBuilder) this; - } - - public DataSourceBuilder url(String url) { - this.properties.put("url", url); - return this; - } - - public DataSourceBuilder driverClassName(String driverClassName) { - this.properties.put("driverClassName", driverClassName); - return this; - } - - public DataSourceBuilder username(String username) { - this.properties.put("username", username); - return this; - } - - public DataSourceBuilder password(String password) { - this.properties.put("password", password); - return this; - } - - public static Class findType(ClassLoader classLoader) { - DataSourceSettings preferredDataSourceSettings = new DataSourceSettingsResolver(classLoader) - .getPreferredDataSourceSettings(); - return (preferredDataSourceSettings != null) ? preferredDataSourceSettings.getType() : null; - } - - private Class getType() { - if (this.type != null) { - return this.type; - } - DataSourceSettings preferredDataSourceSettings = this.settingsResolver.getPreferredDataSourceSettings(); - if (preferredDataSourceSettings != null) { - return preferredDataSourceSettings.getType(); - } - throw new IllegalStateException("No supported DataSource type found"); - } - - private static class DataSourceSettings { - - private final Class type; - - private final Consumer aliasesCustomizer; - - DataSourceSettings(Class type, - Consumer aliasesCustomizer) { - this.type = type; - this.aliasesCustomizer = aliasesCustomizer; - } - - DataSourceSettings(Class type) { - this(type, (aliases) -> { - }); - } - - Class getType() { - return this.type; - } - - void registerAliases(DataSource candidate, ConfigurationPropertyNameAliases aliases) { - if (this.type != null && this.type.isInstance(candidate)) { - this.aliasesCustomizer.accept(aliases); + /** + * Create a new {@link DataSourceBuilder} instance derived from the specified data + * source. The returned builder can be used to build the same type of + * {@link DataSource} with {@code username}, {@code password}, {@code url} and + * {@code driverClassName} properties copied from the original when not specifically + * set. + * @param dataSource the source {@link DataSource} + * @return a new {@link DataSource} builder + * @since 2.5.0 + */ + public static DataSourceBuilder derivedFrom(DataSource dataSource) { + if (dataSource instanceof EmbeddedDatabase) { + try { + dataSource = dataSource.unwrap(DataSource.class); + } + catch (SQLException ex) { + throw new IllegalStateException("Unable to unwap embedded database", ex); } } - + return new DataSourceBuilder<>(dataSource); } - private static class OracleDataSourceSettings extends DataSourceSettings { + /** + * Find the {@link DataSource} type preferred for the given classloader. + * @param classLoader the classloader used to discover preferred settings + * @return the preferred {@link DataSource} type + */ + public static Class findType(ClassLoader classLoader) { + MappedDataSourceProperties mappings = MappedDataSourceProperties.forType(classLoader, null); + return (mappings != null) ? mappings.getDataSourceInstanceType() : null; + } - OracleDataSourceSettings(Class type) { - super(type, (aliases) -> aliases.addAliases("username", "user")); + /** + * An individual DataSource property supported by the builder. + */ + private enum DataSourceProperty { + + URL("url"), + + DRIVER_CLASS_NAME("driverClassName"), + + USERNAME("username"), + + PASSWORD("password"); + + private final String name; + + DataSourceProperty(String name) { + this.name = name; } @Override - public Class getType() { - return null; // Base interface + public String toString() { + return this.name; + } + + Method findSetter(Class type) { + return ReflectionUtils.findMethod(type, "set" + StringUtils.capitalize(this.name), String.class); + } + + Method findGetter(Class type) { + return ReflectionUtils.findMethod(type, "get" + StringUtils.capitalize(this.name), String.class); } } - private static class DataSourceSettingsResolver { + private interface DataSourceProperties { - private final DataSourceSettings preferredDataSourceSettings; + Class getDataSourceInstanceType(); - private final List allDataSourceSettings; + boolean canSet(DataSourceProperty property); - DataSourceSettingsResolver(ClassLoader classLoader) { - List supportedProviders = resolveAvailableDataSourceSettings(classLoader); - this.preferredDataSourceSettings = (!supportedProviders.isEmpty()) ? supportedProviders.get(0) : null; - this.allDataSourceSettings = new ArrayList<>(supportedProviders); - addIfAvailable(this.allDataSourceSettings, - create(classLoader, "org.springframework.jdbc.datasource.SimpleDriverDataSource", - (type) -> new DataSourceSettings(type, - (aliases) -> aliases.addAliases("driver-class-name", "driver-class")))); - addIfAvailable(this.allDataSourceSettings, - create(classLoader, "oracle.jdbc.datasource.OracleDataSource", OracleDataSourceSettings::new)); - addIfAvailable(this.allDataSourceSettings, create(classLoader, "org.h2.jdbcx.JdbcDataSource", - (type) -> new DataSourceSettings(type, (aliases) -> aliases.addAliases("username", "user")))); - addIfAvailable(this.allDataSourceSettings, create(classLoader, "org.postgresql.ds.PGSimpleDataSource", - (type) -> new DataSourceSettings(type, (aliases) -> aliases.addAliases("username", "user")))); + void set(T dataSource, DataSourceProperty property, String value); + + String get(T dataSource, DataSourceProperty property); + + static DataSourceProperties forType(ClassLoader classLoader, Class type) { + MappedDataSourceProperties mapped = MappedDataSourceProperties.forType(classLoader, type); + return (mapped != null) ? mapped : new ReflectionDataSourceProperties<>(type); } - private static List resolveAvailableDataSourceSettings(ClassLoader classLoader) { - List providers = new ArrayList<>(); - addIfAvailable(providers, create(classLoader, "com.zaxxer.hikari.HikariDataSource", - (type) -> new DataSourceSettings(type, (aliases) -> aliases.addAliases("url", "jdbc-url")))); - addIfAvailable(providers, - create(classLoader, "org.apache.tomcat.jdbc.pool.DataSource", DataSourceSettings::new)); - addIfAvailable(providers, - create(classLoader, "org.apache.commons.dbcp2.BasicDataSource", DataSourceSettings::new)); - addIfAvailable(providers, create(classLoader, "oracle.ucp.jdbc.PoolDataSourceImpl", (type) -> { - // Unfortunately Oracle UCP has an import on the Oracle driver itself - if (ClassUtils.isPresent("oracle.jdbc.OracleConnection", classLoader)) { - return new DataSourceSettings(type, (aliases) -> { - aliases.addAliases("username", "user"); - aliases.addAliases("driver-class-name", "connection-factory-class-name"); - }); - } - return null; - })); - return providers; + } + + private static class MappedDataSourceProperties implements DataSourceProperties { + + private final Map> mappedProperties = new HashMap<>(); + + private final Class dataSourceType; + + @SuppressWarnings("unchecked") + MappedDataSourceProperties() { + this.dataSourceType = (Class) ResolvableType.forClass(MappedDataSourceProperties.class, getClass()) + .resolveGeneric(); + } + + @Override + public Class getDataSourceInstanceType() { + return this.dataSourceType; + } + + protected void add(DataSourceProperty property, Getter getter, Setter setter) { + add(property, String.class, getter, setter); + } + + protected void add(DataSourceProperty property, Class type, Getter getter, Setter setter) { + this.mappedProperties.put(property, new MappedDataSourceProperty<>(property, type, getter, setter)); + } + + @Override + public boolean canSet(DataSourceProperty property) { + return this.mappedProperties.containsKey(property); + } + + @Override + public void set(T dataSource, DataSourceProperty property, String value) { + MappedDataSourceProperty mappedProperty = getMapping(property); + mappedProperty.set(dataSource, value); + } + + @Override + public String get(T dataSource, DataSourceProperty property) { + MappedDataSourceProperty mappedProperty = getMapping(property); + return mappedProperty.get(dataSource); + } + + private MappedDataSourceProperty getMapping(DataSourceProperty property) { + MappedDataSourceProperty mappedProperty = this.mappedProperties.get(property); + UnsupportedDataSourcePropertyException.throwIf(mappedProperty == null, + () -> "No mapping found for " + property); + return mappedProperty; + } + + static MappedDataSourceProperties forType(ClassLoader classLoader, Class type) { + MappedDataSourceProperties pooled = lookupPooled(classLoader, type); + if (type == null || pooled != null) { + return pooled; + } + return lookupBasic(classLoader, type); + } + + private static MappedDataSourceProperties lookupPooled(ClassLoader classLoader, + Class type) { + MappedDataSourceProperties result = null; + result = lookup(classLoader, type, result, "com.zaxxer.hikari.HikariDataSource", + HikariDataSourceProperties::new); + result = lookup(classLoader, type, result, "org.apache.tomcat.jdbc.pool.DataSource", + TomcatPoolDataSourceProperties::new); + result = lookup(classLoader, type, result, "org.apache.commons.dbcp2.BasicDataSource", + MappedDbcp2DataSource::new); + result = lookup(classLoader, type, result, "oracle.ucp.jdbc.PoolDataSourceImpl", + OraclePoolDataSourceProperties::new, "oracle.jdbc.OracleConnection"); + return result; + } + + private static MappedDataSourceProperties lookupBasic(ClassLoader classLoader, + Class dataSourceType) { + MappedDataSourceProperties result = null; + result = lookup(classLoader, dataSourceType, result, + "org.springframework.jdbc.datasource.SimpleDriverDataSource", + () -> new SimpleDataSourceProperties()); + result = lookup(classLoader, dataSourceType, result, "oracle.jdbc.datasource.OracleDataSource", + OracleDataSourceProperties::new); + result = lookup(classLoader, dataSourceType, result, "org.h2.jdbcx.JdbcDataSource", + H2DataSourceProperties::new); + result = lookup(classLoader, dataSourceType, result, "org.postgresql.ds.PGSimpleDataSource", + PostgresDataSourceProperties::new); + return result; } @SuppressWarnings("unchecked") - private static DataSourceSettings create(ClassLoader classLoader, String target, - Function, DataSourceSettings> factory) { - if (ClassUtils.isPresent(target, classLoader)) { - try { - Class type = (Class) ClassUtils.forName(target, - classLoader); - return factory.apply(type); - } - catch (Exception ex) { - // Ignore - } + private static MappedDataSourceProperties lookup(ClassLoader classLoader, + Class dataSourceType, MappedDataSourceProperties existing, String dataSourceClassName, + Supplier> propertyMappingsSupplier, String... requiredClassNames) { + if (existing != null || !allPresent(classLoader, dataSourceClassName, requiredClassNames)) { + return existing; } - return null; + MappedDataSourceProperties propertyMappings = propertyMappingsSupplier.get(); + return (dataSourceType == null + || propertyMappings.getDataSourceInstanceType().isAssignableFrom(dataSourceType)) + ? (MappedDataSourceProperties) propertyMappings : null; } - private static void addIfAvailable(Collection list, DataSourceSettings dataSourceSettings) { - if (dataSourceSettings != null) { - list.add(dataSourceSettings); + private static boolean allPresent(ClassLoader classLoader, String dataSourceClassName, + String[] requiredClassNames) { + boolean result = ClassUtils.isPresent(dataSourceClassName, classLoader); + for (String requiredClassName : requiredClassNames) { + result = result && ClassUtils.isPresent(requiredClassName, classLoader); + } + return result; + } + + } + + private static class MappedDataSourceProperty { + + private final DataSourceProperty property; + + private final Class type; + + private final Getter getter; + + private final Setter setter; + + MappedDataSourceProperty(DataSourceProperty property, Class type, Getter getter, Setter setter) { + this.property = property; + this.type = type; + this.getter = getter; + this.setter = setter; + } + + void set(T dataSource, String value) { + try { + UnsupportedDataSourcePropertyException.throwIf(this.setter == null, + () -> "No setter mapped for '" + this.property + "' property"); + this.setter.set(dataSource, convertFromString(value)); + } + catch (SQLException ex) { + throw new IllegalStateException(ex); } } - DataSourceSettings getPreferredDataSourceSettings() { - return this.preferredDataSourceSettings; + String get(T dataSource) { + try { + UnsupportedDataSourcePropertyException.throwIf(this.getter == null, + () -> "No getter mapped for '" + this.property + "' property"); + return convertToString(this.getter.get(dataSource)); + } + catch (SQLException ex) { + throw new IllegalStateException(ex); + } } - void registerAliases(DataSource result, ConfigurationPropertyNameAliases aliases) { - this.allDataSourceSettings.forEach((settings) -> settings.registerAliases(result, aliases)); + @SuppressWarnings("unchecked") + private V convertFromString(String value) { + if (String.class.equals(this.type)) { + return (V) value; + } + if (Class.class.equals(this.type)) { + return (V) ClassUtils.resolveClassName(value, null); + } + throw new IllegalStateException("Unsupported value type " + this.type); + } + + private String convertToString(V value) { + if (String.class.equals(this.type)) { + return (String) value; + } + if (Class.class.equals(this.type)) { + return ((Class) value).getName(); + } + throw new IllegalStateException("Unsupported value type " + this.type); + } + + } + + private static class ReflectionDataSourceProperties implements DataSourceProperties { + + private final Map getters; + + private final Map setters; + + private Class dataSourceType; + + ReflectionDataSourceProperties(Class dataSourceType) { + Assert.state(dataSourceType != null, "No supported DataSource type found"); + Map getters = new HashMap<>(); + Map setters = new HashMap<>(); + for (DataSourceProperty property : DataSourceProperty.values()) { + putIfNotNull(getters, property, property.findGetter(dataSourceType)); + putIfNotNull(setters, property, property.findSetter(dataSourceType)); + } + this.dataSourceType = dataSourceType; + this.getters = Collections.unmodifiableMap(getters); + this.setters = Collections.unmodifiableMap(setters); + } + + private void putIfNotNull(Map map, DataSourceProperty property, Method method) { + if (method != null) { + map.put(property, method); + } + } + + @Override + public Class getDataSourceInstanceType() { + return this.dataSourceType; + } + + @Override + public boolean canSet(DataSourceProperty property) { + return this.setters.containsKey(property); + } + + @Override + public void set(T dataSource, DataSourceProperty property, String value) { + Method method = getMethod(property, this.setters); + ReflectionUtils.invokeMethod(method, dataSource, value); + } + + @Override + public String get(T dataSource, DataSourceProperty property) { + Method method = getMethod(property, this.getters); + return (String) ReflectionUtils.invokeMethod(method, dataSource); + } + + private Method getMethod(DataSourceProperty property, Map setters2) { + Method method = setters2.get(property); + UnsupportedDataSourcePropertyException.throwIf(method == null, + () -> "Unable to find sutable method for " + property); + ReflectionUtils.makeAccessible(method); + return method; + } + + } + + @FunctionalInterface + private interface Getter { + + V get(T instance) throws SQLException; + + } + + @FunctionalInterface + private interface Setter { + + void set(T instance, V value) throws SQLException; + + } + + /** + * {@link MappedDataSource} for Hikari. + */ + private static class HikariDataSourceProperties extends MappedDataSourceProperties { + + HikariDataSourceProperties() { + add(DataSourceProperty.URL, HikariDataSource::getJdbcUrl, HikariDataSource::setJdbcUrl); + add(DataSourceProperty.DRIVER_CLASS_NAME, HikariDataSource::getDriverClassName, + HikariDataSource::setDriverClassName); + add(DataSourceProperty.USERNAME, HikariDataSource::getUsername, HikariDataSource::setUsername); + add(DataSourceProperty.PASSWORD, HikariDataSource::getPassword, HikariDataSource::setPassword); + } + + } + + /** + * {@link MappedDataSource} for Tomcat Pool. + */ + private static class TomcatPoolDataSourceProperties + extends MappedDataSourceProperties { + + TomcatPoolDataSourceProperties() { + add(DataSourceProperty.URL, org.apache.tomcat.jdbc.pool.DataSource::getUrl, + org.apache.tomcat.jdbc.pool.DataSource::setUrl); + add(DataSourceProperty.DRIVER_CLASS_NAME, org.apache.tomcat.jdbc.pool.DataSource::getDriverClassName, + org.apache.tomcat.jdbc.pool.DataSource::setDriverClassName); + add(DataSourceProperty.USERNAME, org.apache.tomcat.jdbc.pool.DataSource::getUsername, + org.apache.tomcat.jdbc.pool.DataSource::setUsername); + add(DataSourceProperty.PASSWORD, org.apache.tomcat.jdbc.pool.DataSource::getPassword, + org.apache.tomcat.jdbc.pool.DataSource::setPassword); + } + + } + + /** + * {@link MappedDataSource} for DBCP2. + */ + private static class MappedDbcp2DataSource extends MappedDataSourceProperties { + + MappedDbcp2DataSource() { + add(DataSourceProperty.URL, BasicDataSource::getUrl, BasicDataSource::setUrl); + add(DataSourceProperty.DRIVER_CLASS_NAME, BasicDataSource::getDriverClassName, + BasicDataSource::setDriverClassName); + add(DataSourceProperty.USERNAME, BasicDataSource::getUsername, BasicDataSource::setUsername); + add(DataSourceProperty.PASSWORD, BasicDataSource::getPassword, BasicDataSource::setPassword); + } + + } + + /** + * {@link MappedDataSource} for Oracle Pool. + */ + private static class OraclePoolDataSourceProperties extends MappedDataSourceProperties { + + @Override + public Class getDataSourceInstanceType() { + return PoolDataSourceImpl.class; + } + + OraclePoolDataSourceProperties() { + add(DataSourceProperty.URL, PoolDataSource::getURL, PoolDataSource::setURL); + add(DataSourceProperty.DRIVER_CLASS_NAME, PoolDataSource::getConnectionFactoryClassName, + PoolDataSource::setConnectionFactoryClassName); + add(DataSourceProperty.USERNAME, PoolDataSource::getUser, PoolDataSource::setUser); + add(DataSourceProperty.PASSWORD, PoolDataSource::getPassword, PoolDataSource::setPassword); + } + + } + + /** + * {@link MappedDataSource} for Spring's {@link SimpleDriverDataSource}. + */ + private static class SimpleDataSourceProperties extends MappedDataSourceProperties { + + @SuppressWarnings("unchecked") + SimpleDataSourceProperties() { + add(DataSourceProperty.URL, SimpleDriverDataSource::getUrl, SimpleDriverDataSource::setUrl); + add(DataSourceProperty.DRIVER_CLASS_NAME, Class.class, (dataSource) -> dataSource.getDriver().getClass(), + (dataSource, driverClass) -> dataSource.setDriverClass(driverClass)); + add(DataSourceProperty.USERNAME, SimpleDriverDataSource::getUsername, SimpleDriverDataSource::setUsername); + add(DataSourceProperty.PASSWORD, SimpleDriverDataSource::getPassword, SimpleDriverDataSource::setPassword); + } + + } + + /** + * {@link MappedDataSource} for Oracle. + */ + private static class OracleDataSourceProperties extends MappedDataSourceProperties { + + OracleDataSourceProperties() { + add(DataSourceProperty.URL, OracleDataSource::getURL, OracleDataSource::setURL); + add(DataSourceProperty.USERNAME, OracleDataSource::getUser, OracleDataSource::setUser); + add(DataSourceProperty.PASSWORD, null, OracleDataSource::setPassword); + } + + } + + /** + * {@link MappedDataSource} for H2. + */ + private static class H2DataSourceProperties extends MappedDataSourceProperties { + + H2DataSourceProperties() { + add(DataSourceProperty.URL, JdbcDataSource::getUrl, JdbcDataSource::setUrl); + add(DataSourceProperty.USERNAME, JdbcDataSource::getUser, JdbcDataSource::setUser); + add(DataSourceProperty.PASSWORD, JdbcDataSource::getPassword, JdbcDataSource::setPassword); + } + + } + + /** + * {@link MappedDataSource} for Postgres. + */ + private static class PostgresDataSourceProperties extends MappedDataSourceProperties { + + PostgresDataSourceProperties() { + add(DataSourceProperty.URL, PGSimpleDataSource::getUrl, PGSimpleDataSource::setUrl); + add(DataSourceProperty.USERNAME, PGSimpleDataSource::getUser, PGSimpleDataSource::setUser); + add(DataSourceProperty.PASSWORD, PGSimpleDataSource::getPassword, PGSimpleDataSource::setPassword); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/UnsupportedDataSourcePropertyException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/UnsupportedDataSourcePropertyException.java new file mode 100644 index 0000000000..561dea3fbd --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/UnsupportedDataSourcePropertyException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.springframework.boot.jdbc; + +import java.util.function.Supplier; + +/** + * {@link RuntimeException} thrown from {@link DataSourceBuilder} when an unsupported + * property is used. + * + * @author Phillip Webb + * @since 2.5.0 + */ +public class UnsupportedDataSourcePropertyException extends RuntimeException { + + UnsupportedDataSourcePropertyException(String message) { + super(message); + } + + static void throwIf(boolean test, Supplier message) { + if (test) { + throw new UnsupportedDataSourcePropertyException(message.get()); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderNoHikariTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderNoHikariTests.java new file mode 100644 index 0000000000..8e6606ff6e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderNoHikariTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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.springframework.boot.jdbc; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DataSourceBuilder} when Hikari is not on the classpath. + * + * @author Phillip Webb + */ +@ClassPathExclusions("Hikari*.jar") +class DataSourceBuilderNoHikariTests { + + @Test + void findTypeReturnsTomcatDataSource() { + assertThat(DataSourceBuilder.findType(null)).isEqualTo(org.apache.tomcat.jdbc.pool.DataSource.class); + } + + @Test + void createAndBuildReturnsTomcatDataSource() { + DataSource dataSource = DataSourceBuilder.create().build(); + assertThat(dataSource).isInstanceOf(org.apache.tomcat.jdbc.pool.DataSource.class); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java index 974bced9bc..96a20801b2 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/DataSourceBuilderTests.java @@ -20,12 +20,14 @@ import java.io.Closeable; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; +import java.sql.Connection; import java.sql.SQLException; import java.util.Arrays; import javax.sql.DataSource; import com.zaxxer.hikari.HikariDataSource; +import oracle.jdbc.internal.OpaqueString; import oracle.jdbc.pool.OracleDataSource; import oracle.ucp.jdbc.PoolDataSourceImpl; import org.apache.commons.dbcp2.BasicDataSource; @@ -35,15 +37,21 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.jdbc.datasource.AbstractDataSource; import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Tests for {@link DataSourceBuilder}. * * @author Stephane Nicoll * @author Fabio Grassi + * @author Phillip Webb */ class DataSourceBuilderTests { @@ -57,7 +65,7 @@ class DataSourceBuilderTests { } @Test - void defaultToHikari() { + void buildWhenHikariAvailableReturnsHikariDataSource() { this.dataSource = DataSourceBuilder.create().url("jdbc:h2:test").build(); assertThat(this.dataSource).isInstanceOf(HikariDataSource.class); HikariDataSource hikariDataSource = (HikariDataSource) this.dataSource; @@ -65,14 +73,14 @@ class DataSourceBuilderTests { } @Test - void defaultToTomcatIfHikariIsNotAvailable() { + void buildWhenHikariNotAvailableReturnsTomcatDataSource() { this.dataSource = DataSourceBuilder.create(new HidePackagesClassLoader("com.zaxxer.hikari")).url("jdbc:h2:test") .build(); assertThat(this.dataSource).isInstanceOf(org.apache.tomcat.jdbc.pool.DataSource.class); } @Test - void defaultToCommonsDbcp2IfNeitherHikariNorTomcatIsNotAvailable() { + void buildWhenHikariAndTomcatNotAvailableReturnsDbcp2DataSource() { this.dataSource = DataSourceBuilder .create(new HidePackagesClassLoader("com.zaxxer.hikari", "org.apache.tomcat.jdbc.pool")) .url("jdbc:h2:test").build(); @@ -80,20 +88,20 @@ class DataSourceBuilderTests { } @Test - void defaultToOracleUcpAsLastResort() { + void buildWhenHikariAndTomcatAndDbcpNotAvailableReturnsOracleUcpDataSource() { this.dataSource = DataSourceBuilder.create(new HidePackagesClassLoader("com.zaxxer.hikari", "org.apache.tomcat.jdbc.pool", "org.apache.commons.dbcp2")).url("jdbc:h2:test").build(); assertThat(this.dataSource).isInstanceOf(PoolDataSourceImpl.class); } @Test - void specificTypeOfDataSource() { + void buildWhenHikariTypeSpecifiedReturnsExpectedDataSource() { HikariDataSource hikariDataSource = DataSourceBuilder.create().type(HikariDataSource.class).build(); assertThat(hikariDataSource).isInstanceOf(HikariDataSource.class); } @Test - void dataSourceCanBeCreatedWithSimpleDriverDataSource() { + void buildWhenSimpleDriverTypeSpecifiedReturnsExpectedDataSource() { this.dataSource = DataSourceBuilder.create().url("jdbc:h2:test").type(SimpleDriverDataSource.class).build(); assertThat(this.dataSource).isInstanceOf(SimpleDriverDataSource.class); SimpleDriverDataSource simpleDriverDataSource = (SimpleDriverDataSource) this.dataSource; @@ -102,7 +110,7 @@ class DataSourceBuilderTests { } @Test - void dataSourceCanBeCreatedWithOracleDataSource() throws SQLException { + void buildWhenOracleTypeSpecifiedReturnsExpectedDataSource() throws SQLException { this.dataSource = DataSourceBuilder.create().url("jdbc:oracle:thin:@localhost:1521:xe") .type(OracleDataSource.class).username("test").build(); assertThat(this.dataSource).isInstanceOf(OracleDataSource.class); @@ -112,7 +120,7 @@ class DataSourceBuilderTests { } @Test - void dataSourceCanBeCreatedWithOracleUcpDataSource() { + void buildWhenOracleUcpTypeSpecifiedReturnsExpectedDataSource() { this.dataSource = DataSourceBuilder.create().driverClassName("org.hsqldb.jdbc.JDBCDriver") .type(PoolDataSourceImpl.class).username("test").build(); assertThat(this.dataSource).isInstanceOf(PoolDataSourceImpl.class); @@ -122,7 +130,7 @@ class DataSourceBuilderTests { } @Test - void dataSourceCanBeCreatedWithH2JdbcDataSource() { + void buildWhenH2TypeSpecifiedReturnsExpectedDataSource() { this.dataSource = DataSourceBuilder.create().url("jdbc:h2:test").type(JdbcDataSource.class).username("test") .build(); assertThat(this.dataSource).isInstanceOf(JdbcDataSource.class); @@ -131,7 +139,7 @@ class DataSourceBuilderTests { } @Test - void dataSourceCanBeCreatedWithPGDataSource() { + void buildWhenPostgressTypeSpecifiedReturnsExpectedDataSource() { this.dataSource = DataSourceBuilder.create().url("jdbc:postgresql://localhost/test") .type(PGSimpleDataSource.class).username("test").build(); assertThat(this.dataSource).isInstanceOf(PGSimpleDataSource.class); @@ -140,11 +148,17 @@ class DataSourceBuilderTests { } @Test - void dataSourceAliasesAreOnlyAppliedToRelevantDataSource() { - this.dataSource = DataSourceBuilder.create().url("jdbc:h2:test").type(TestDataSource.class).username("test") - .build(); - assertThat(this.dataSource).isInstanceOf(TestDataSource.class); - TestDataSource testDataSource = (TestDataSource) this.dataSource; + void buildWhenMappedTypeSpecifiedAndNoSuitableMappingThrowsException() { + assertThatExceptionOfType(UnsupportedDataSourcePropertyException.class).isThrownBy( + () -> DataSourceBuilder.create().type(OracleDataSource.class).driverClassName("com.example").build()); + } + + @Test + void buildWhenCustomSubclassTypeSpecifiedReturnsDataSourceWithOnlyBasePropertiesSet() { + this.dataSource = DataSourceBuilder.create().url("jdbc:h2:test").type(CustomTomcatDataSource.class) + .username("test").build(); + assertThat(this.dataSource).isInstanceOf(CustomTomcatDataSource.class); + CustomTomcatDataSource testDataSource = (CustomTomcatDataSource) this.dataSource; assertThat(testDataSource.getUrl()).isEqualTo("jdbc:h2:test"); assertThat(testDataSource.getJdbcUrl()).isNull(); assertThat(testDataSource.getUsername()).isEqualTo("test"); @@ -153,6 +167,85 @@ class DataSourceBuilderTests { assertThat(testDataSource.getDriverClass()).isNull(); } + @Test + void buildWhenCustomTypeSpecifiedReturnsDataSourceWithPropertiesSetViaReflection() { + this.dataSource = DataSourceBuilder.create().type(CustomDataSource.class).username("test").password("secret") + .url("jdbc:h2:test").driverClassName("com.example").build(); + assertThat(this.dataSource).isInstanceOf(CustomDataSource.class); + CustomDataSource testDataSource = (CustomDataSource) this.dataSource; + assertThat(testDataSource.getUrl()).isEqualTo("jdbc:h2:test"); + assertThat(testDataSource.getUsername()).isEqualTo("test"); + assertThat(testDataSource.getPassword()).isEqualTo("secret"); + assertThat(testDataSource.getDriverClassName()).isEqualTo("com.example"); + } + + @Test + void buildWhenCustomTypeSpecifiedAndNoSuitableSetterThrowsException() { + assertThatExceptionOfType(UnsupportedDataSourcePropertyException.class).isThrownBy(() -> DataSourceBuilder + .create().type(LimitedCustomDataSource.class).driverClassName("com.example").build()); + } + + @Test + void buildWhenDerivedWithNewUrlReturnsNewDataSource() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setUsername("test"); + dataSource.setPassword("secret"); + dataSource.setJdbcUrl("jdbc:h2:test"); + HikariDataSource built = (HikariDataSource) DataSourceBuilder.derivedFrom(dataSource).url("jdbc:h2:test2") + .build(); + assertThat(built.getUsername()).isEqualTo("test"); + assertThat(built.getPassword()).isEqualTo("secret"); + assertThat(built.getJdbcUrl()).isEqualTo("jdbc:h2:test2"); + } + + @Test + void buildWhenDerivedWithNewUsernameAndPasswordReturnsNewDataSource() { + HikariDataSource dataSource = new HikariDataSource(); + dataSource.setUsername("test"); + dataSource.setPassword("secret"); + dataSource.setJdbcUrl("jdbc:h2:test"); + DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource); + HikariDataSource built = (HikariDataSource) builder.username("test2").password("secret2").build(); + assertThat(built.getUsername()).isEqualTo("test2"); + assertThat(built.getPassword()).isEqualTo("secret2"); + assertThat(built.getJdbcUrl()).isEqualTo("jdbc:h2:test"); + } + + @Test + void buildWhenDerivedFromOracleDataSourceWithPasswordNotSetThrowsException() throws Exception { + oracle.jdbc.datasource.impl.OracleDataSource dataSource = new oracle.jdbc.datasource.impl.OracleDataSource(); + dataSource.setUser("test"); + dataSource.setPassword("secret"); + dataSource.setURL("example.com"); + assertThatExceptionOfType(UnsupportedDataSourcePropertyException.class) + .isThrownBy(() -> DataSourceBuilder.derivedFrom(dataSource).url("example.org").build()); + } + + @Test + void buildWhenDerivedFromOracleDataSourceWithPasswordSetReturnsDataSource() throws Exception { + oracle.jdbc.datasource.impl.OracleDataSource dataSource = new oracle.jdbc.datasource.impl.OracleDataSource(); + dataSource.setUser("test"); + dataSource.setPassword("secret"); + dataSource.setURL("example.com"); + DataSourceBuilder builder = DataSourceBuilder.derivedFrom(dataSource); + oracle.jdbc.datasource.impl.OracleDataSource built = (oracle.jdbc.datasource.impl.OracleDataSource) builder + .username("test2").password("secret2").build(); + assertThat(built.getUser()).isEqualTo("test2"); + assertThat(built).extracting("password").extracting((opaque) -> ((OpaqueString) opaque).get()) + .isEqualTo("secret2"); + assertThat(built.getURL()).isEqualTo("example.com"); + } + + @Test + void buildWhenDerivedFromEmbeddedDatabase() { + EmbeddedDatabase database = new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL).build(); + SimpleDriverDataSource built = (SimpleDriverDataSource) DataSourceBuilder.derivedFrom(database).username("test") + .password("secret").build(); + assertThat(built.getUsername()).isEqualTo("test"); + assertThat(built.getPassword()).isEqualTo("secret"); + assertThat(built.getUrl()).startsWith("jdbc:hsqldb:mem"); + } + final class HidePackagesClassLoader extends URLClassLoader { private final String[] hiddenPackages; @@ -172,7 +265,7 @@ class DataSourceBuilderTests { } - public static class TestDataSource extends org.apache.tomcat.jdbc.pool.DataSource { + static class CustomTomcatDataSource extends org.apache.tomcat.jdbc.pool.DataSource { private String jdbcUrl; @@ -180,30 +273,88 @@ class DataSourceBuilderTests { private String driverClass; - public String getJdbcUrl() { + String getJdbcUrl() { return this.jdbcUrl; } - public void setJdbcUrl(String jdbcUrl) { + void setJdbcUrl(String jdbcUrl) { this.jdbcUrl = jdbcUrl; } - public String getUser() { + String getUser() { return this.user; } - public void setUser(String user) { + void setUser(String user) { this.user = user; } - public String getDriverClass() { + String getDriverClass() { return this.driverClass; } - public void setDriverClass(String driverClass) { + void setDriverClass(String driverClass) { this.driverClass = driverClass; } } + static class LimitedCustomDataSource extends AbstractDataSource { + + private String username; + + private String password; + + private String url; + + @Override + public Connection getConnection() throws SQLException { + throw new UnsupportedOperationException(); + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + throw new UnsupportedOperationException(); + } + + String getUsername() { + return this.username; + } + + void setUsername(String username) { + this.username = username; + } + + String getPassword() { + return this.password; + } + + void setPassword(String password) { + this.password = password; + } + + String getUrl() { + return this.url; + } + + void setUrl(String url) { + this.url = url; + } + + } + + static class CustomDataSource extends LimitedCustomDataSource { + + private String driverClassName; + + String getDriverClassName() { + return this.driverClassName; + } + + void setDriverClassName(String driverClassName) { + this.driverClassName = driverClassName; + } + + } + }