Merge pull request #239 from spikymonkey/url_decoding_data_source

Handle drivers which don't support URL-encoding
This commit is contained in:
Scott Frederick
2018-11-07 10:42:20 -06:00
committed by GitHub
11 changed files with 410 additions and 38 deletions

View File

@@ -21,6 +21,6 @@ public class BasicDbcpPooledDataSourceCreator<SI extends RelationalServiceInfo>
logger.info("Found DBCP2 on the classpath. Using it for DataSource connection pooling.");
org.apache.commons.dbcp2.BasicDataSource ds = new org.apache.commons.dbcp2.BasicDataSource();
setBasicDataSourceProperties(ds, serviceInfo, serviceConnectorConfig, driverClassName, validationQuery);
return ds;
return new UrlDecodingDataSource(ds, "url");
}
}

View File

@@ -1,6 +1,5 @@
package org.springframework.cloud.service.relational;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
@@ -15,7 +14,6 @@ import org.springframework.cloud.service.AbstractServiceConnectorCreator;
import org.springframework.cloud.service.ServiceConnectorConfig;
import org.springframework.cloud.service.ServiceConnectorCreationException;
import org.springframework.cloud.service.common.RelationalServiceInfo;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
/**
*
@@ -32,7 +30,7 @@ public abstract class DataSourceCreator<SI extends RelationalServiceInfo> extend
private String validationQuery;
private Map<String, PooledDataSourceCreator<SI>> pooledDataSourceCreators =
new LinkedHashMap<String, PooledDataSourceCreator<SI>>();
new LinkedHashMap<>();
public DataSourceCreator(String driverSystemPropKey, String[] driverClasses, String validationQuery) {
this.driverSystemPropKey = driverSystemPropKey;
@@ -40,10 +38,10 @@ public abstract class DataSourceCreator<SI extends RelationalServiceInfo> extend
this.validationQuery = validationQuery;
if (pooledDataSourceCreators.size() == 0) {
putPooledDataSourceCreator(new TomcatJdbcPooledDataSourceCreator<SI>());
putPooledDataSourceCreator(new HikariCpPooledDataSourceCreator<SI>());
putPooledDataSourceCreator(new TomcatDbcpPooledDataSourceCreator<SI>());
putPooledDataSourceCreator(new BasicDbcpPooledDataSourceCreator<SI>());
putPooledDataSourceCreator(new TomcatJdbcPooledDataSourceCreator<>());
putPooledDataSourceCreator(new HikariCpPooledDataSourceCreator<>());
putPooledDataSourceCreator(new TomcatDbcpPooledDataSourceCreator<>());
putPooledDataSourceCreator(new BasicDbcpPooledDataSourceCreator<>());
}
}
@@ -60,7 +58,7 @@ public abstract class DataSourceCreator<SI extends RelationalServiceInfo> extend
}
// Only for testing outside Tomcat/CloudFoundry
logger.warning("No connection pooling DataSource implementation found on the classpath - no pooling is in effect.");
return new SimpleDriverDataSource(DriverManager.getDriver(serviceInfo.getJdbcUrl()), serviceInfo.getJdbcUrl());
return new UrlDecodingDataSource(serviceInfo.getJdbcUrl());
} catch (Exception e) {
throw new ServiceConnectorCreationException(
"Failed to created cloud datasource for " + serviceInfo.getId() + " service", e);
@@ -84,7 +82,7 @@ public abstract class DataSourceCreator<SI extends RelationalServiceInfo> extend
if (serviceConnectorConfig != null) {
List<String> pooledDataSourceNames = ((DataSourceConfig) serviceConnectorConfig).getPooledDataSourceNames();
if (pooledDataSourceNames != null) {
List<PooledDataSourceCreator<SI>> filtered = new ArrayList<PooledDataSourceCreator<SI>>();
List<PooledDataSourceCreator<SI>> filtered = new ArrayList<>();
for (String name : pooledDataSourceNames) {
for (String key : pooledDataSourceCreators.keySet()) {

View File

@@ -1,17 +1,16 @@
package org.springframework.cloud.service.relational;
import static org.springframework.cloud.service.Util.*;
import java.util.logging.Logger;
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.cloud.service.ServiceConnectorConfig;
import org.springframework.cloud.service.common.RelationalServiceInfo;
import com.zaxxer.hikari.HikariDataSource;
import static org.springframework.cloud.service.Util.hasClass;
public class HikariCpPooledDataSourceCreator<SI extends RelationalServiceInfo> implements PooledDataSourceCreator<SI> {
@@ -41,7 +40,7 @@ public class HikariCpPooledDataSourceCreator<SI extends RelationalServiceInfo> i
logger.info("Found HikariCP on the classpath. Using it for DataSource connection pooling.");
HikariDataSource ds = new HikariDataSource();
setBasicDataSourceProperties(ds, serviceInfo, serviceConnectorConfig, driverClassName, validationQuery);
return ds;
return new UrlDecodingDataSource(ds, "jdbcUrl");
} else {
return null;
}

View File

@@ -1,13 +1,13 @@
package org.springframework.cloud.service.relational;
import static org.springframework.cloud.service.Util.hasClass;
import javax.sql.DataSource;
import org.springframework.cloud.service.ServiceConnectorConfig;
import org.springframework.cloud.service.ServiceConnectorCreationException;
import org.springframework.cloud.service.common.RelationalServiceInfo;
import static org.springframework.cloud.service.Util.hasClass;
/**
*
* @author Ramnivas Laddad
@@ -39,7 +39,7 @@ public class TomcatDbcpPooledDataSourceCreator<SI extends RelationalServiceInfo>
try {
DataSource dataSource = (DataSource) Class.forName(className).newInstance();
setBasicDataSourceProperties(dataSource, serviceInfo, serviceConnectorConfig, driverClassName, validationQuery);
return dataSource;
return new UrlDecodingDataSource(dataSource, "url");
} catch (Throwable e) {
throw new ServiceConnectorCreationException("Error instantiating Tomcat DBCP connection pool", e);
}

View File

@@ -1,12 +1,12 @@
package org.springframework.cloud.service.relational;
import static org.springframework.cloud.service.Util.hasClass;
import javax.sql.DataSource;
import org.springframework.cloud.service.ServiceConnectorConfig;
import org.springframework.cloud.service.common.RelationalServiceInfo;
import static org.springframework.cloud.service.Util.hasClass;
/**
*
* @author Ramnivas Laddad
@@ -23,7 +23,7 @@ public class TomcatJdbcPooledDataSourceCreator<SI extends RelationalServiceInfo>
logger.info("Found Tomcat JDBC connection pool on the classpath. Using it for DataSource connection pooling.");
org.apache.tomcat.jdbc.pool.DataSource ds = new org.apache.tomcat.jdbc.pool.DataSource();
setBasicDataSourceProperties(ds, serviceInfo, serviceConnectorConfig, driverClassName, validationQuery);
return ds;
return new UrlDecodingDataSource(ds, "url");
} else {
return null;
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.service.relational;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.function.Function;
import java.util.logging.Logger;
import javax.sql.DataSource;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.jdbc.datasource.DelegatingDataSource;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
/**
* <p>
* {@link UrlDecodingDataSource} is used to transparently handle combinations of Clouds and database
* drivers which may or may not provide or accept URL-encoded connection strings.
* </p>
*
* <p>
* This {@link DataSource} implementation transparently delegates to an underlying {@link DataSource},
* except in the case where a connection attempt is made and no previous connection attempt has been
* successful. In this case, if the underlying {@link DataSource} fails to connect,
* {@link UrlDecodingDataSource} makes a test connection using a URL-decoded version of the configured
* JDBC URL. If this is successful, the underlying {@link DataSource} is updated with the decoded JDBC
* URL, and used to establish the final connection which is returned to the client.
* </p>
*
* @author Gareth Clay
*/
class UrlDecodingDataSource extends DelegatingDataSource {
private static final Logger logger = Logger.getLogger(DelegatingDataSource.class.getName());
private final String urlPropertyName;
private final Function<String, DataSource> connectionTestDataSourceFactory;
private volatile boolean successfullyConnected = false;
UrlDecodingDataSource(String jdbcUrl) {
this(newSimpleDriverDataSource(jdbcUrl), "url");
}
UrlDecodingDataSource(DataSource targetDataSource, String urlPropertyName) {
this(targetDataSource, urlPropertyName, UrlDecodingDataSource::newSimpleDriverDataSource);
}
UrlDecodingDataSource(DataSource targetDataSource, String urlPropertyName, Function<String, DataSource> connectionTestDataSourceFactory) {
super(targetDataSource);
this.urlPropertyName = urlPropertyName;
this.connectionTestDataSourceFactory = connectionTestDataSourceFactory;
}
@Override
public Connection getConnection() throws SQLException {
if (successfullyConnected) return super.getConnection();
synchronized (this) {
Connection connection;
try {
connection = super.getConnection();
successfullyConnected = true;
} catch (SQLException e) {
logger.info("Database connection failed. Trying again with url-decoded jdbc url");
DataSource targetDataSource = getTargetDataSource();
if (targetDataSource == null) throw new IllegalStateException("target DataSource should never be null");
BeanWrapper dataSourceWrapper = new BeanWrapperImpl(targetDataSource);
String decodedJdbcUrl = decode((String) dataSourceWrapper.getPropertyValue(urlPropertyName));
DataSource urlDecodedConnectionTestDataSource = connectionTestDataSourceFactory.apply(decodedJdbcUrl);
urlDecodedConnectionTestDataSource.getConnection();
logger.info("Connection test successful. Continuing with url-decoded jdbc url");
dataSourceWrapper.setPropertyValue(urlPropertyName, decodedJdbcUrl);
connection = super.getConnection();
successfullyConnected = true;
}
return connection;
}
}
private static DataSource newSimpleDriverDataSource(String jdbcUrl) {
try {
return new SimpleDriverDataSource(DriverManager.getDriver(jdbcUrl), jdbcUrl);
} catch (SQLException e) {
throw new RuntimeException("Unable to construct DataSource", e);
}
}
private static String decode(String string) {
try {
return URLDecoder.decode(string, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
return string;
}
}
}

View File

@@ -1,12 +1,15 @@
package org.springframework.cloud.config;
import static org.junit.Assert.assertEquals;
import java.util.Properties;
import javax.sql.DataSource;
import org.springframework.cloud.ReflectionUtils;
import org.springframework.jdbc.datasource.DelegatingDataSource;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
/**
*
@@ -16,14 +19,35 @@ import org.springframework.cloud.ReflectionUtils;
public class DataSourceCloudConfigTestHelper extends CommonPoolCloudConfigTestHelper {
public static void assertPoolProperties(DataSource dataSource, int maxActive, int minIdle, long maxWait) {
if (dataSource instanceof DelegatingDataSource) {
dataSource = ((DelegatingDataSource) dataSource).getTargetDataSource();
}
assertCommonsPoolProperties(dataSource, maxActive, minIdle, maxWait);
}
public static void assertConnectionProperties(DataSource dataSource, Properties connectionProp) {
if (dataSource instanceof DelegatingDataSource) {
dataSource = ((DelegatingDataSource) dataSource).getTargetDataSource();
}
assertEquals(connectionProp, ReflectionUtils.getValue(dataSource, "connectionProperties"));
}
public static void assertConnectionProperty(DataSource dataSource, String key, Object value) {
if (dataSource instanceof DelegatingDataSource) {
dataSource = ((DelegatingDataSource) dataSource).getTargetDataSource();
}
assertEquals(value, ReflectionUtils.getValue(dataSource, key));
}
public static void assertDataSourceType(DataSource dataSource, String className) throws ClassNotFoundException {
assertDataSourceType(dataSource, Class.forName(className));
}
public static void assertDataSourceType(DataSource dataSource, Class clazz) throws ClassNotFoundException {
if (dataSource instanceof DelegatingDataSource) {
dataSource = ((DelegatingDataSource) dataSource).getTargetDataSource();
}
assertThat(dataSource, instanceOf(clazz));
}
}

View File

@@ -1,10 +1,10 @@
package org.springframework.cloud.config.xml;
import java.util.Properties;
import javax.sql.DataSource;
import org.junit.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.cloud.config.DataSourceCloudConfigTestHelper;
import org.springframework.cloud.service.ServiceInfo;
@@ -13,10 +13,9 @@ import org.springframework.cloud.service.relational.TomcatJdbcPooledDataSourceCr
import org.springframework.context.ApplicationContext;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertThat;
import static org.springframework.cloud.config.DataSourceCloudConfigTestHelper.assertConnectionProperties;
import static org.springframework.cloud.config.DataSourceCloudConfigTestHelper.assertConnectionProperty;
import static org.springframework.cloud.config.DataSourceCloudConfigTestHelper.assertDataSourceType;
/**
*
@@ -109,7 +108,7 @@ public abstract class DataSourceXmlConfigTest extends AbstractServiceXmlConfigTe
createService("my-service"));
DataSource ds = testContext.getBean("db-pool-tomcat-jdbc", getConnectorType());
assertThat(ds, instanceOf(Class.forName(TomcatJdbcPooledDataSourceCreator.TOMCAT_JDBC_DATASOURCE)));
assertDataSourceType(ds, TomcatJdbcPooledDataSourceCreator.TOMCAT_JDBC_DATASOURCE);
}
@Test
@@ -118,7 +117,7 @@ public abstract class DataSourceXmlConfigTest extends AbstractServiceXmlConfigTe
createService("my-service"));
DataSource ds = testContext.getBean("db-pool-hikari", getConnectorType());
assertThat(ds, instanceOf(Class.forName(HikariCpPooledDataSourceCreator.HIKARI_DATASOURCE)));
assertDataSourceType(ds, HikariCpPooledDataSourceCreator.HIKARI_DATASOURCE);
}
@Test
@@ -127,6 +126,6 @@ public abstract class DataSourceXmlConfigTest extends AbstractServiceXmlConfigTe
createService("my-service"));
DataSource ds = testContext.getBean("db-pool-invalid", getConnectorType());
assertThat(ds, instanceOf(SimpleDriverDataSource.class));
assertDataSourceType(ds, SimpleDriverDataSource.class);
}
}

View File

@@ -1,23 +1,24 @@
package org.springframework.cloud.service.relational;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import javax.sql.DataSource;
import org.junit.Test;
import org.springframework.cloud.ReflectionUtils;
import org.springframework.cloud.config.DataSourceCloudConfigTestHelper;
import org.springframework.cloud.service.PooledServiceConnectorConfig.PoolConfig;
import org.springframework.cloud.service.common.RelationalServiceInfo;
import org.springframework.cloud.service.relational.DataSourceConfig.ConnectionConfig;
import org.springframework.jdbc.datasource.DelegatingDataSource;
import org.springframework.test.util.ReflectionTestUtils;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
*
* @author Ramnivas Laddad
@@ -59,12 +60,21 @@ public abstract class AbstractDataSourceCreatorTest<C extends DataSourceCreator<
}
private void assertConnectionProperties(DataSource dataSource, Properties connectionProp) {
if (dataSource instanceof DelegatingDataSource) {
dataSource = ((DelegatingDataSource) dataSource).getTargetDataSource();
}
assertEquals(connectionProp, ReflectionTestUtils.getField(dataSource, "connectionProperties"));
}
private void assertDataSourceProperties(RelationalServiceInfo relationalServiceInfo, DataSource dataSource) {
assertNotNull(dataSource);
if (dataSource instanceof DelegatingDataSource) {
dataSource = ((DelegatingDataSource) dataSource).getTargetDataSource();
}
assertNotNull(dataSource);
assertEquals(getDriverName(), ReflectionUtils.getValue(dataSource, "driverClassName"));
assertEquals(relationalServiceInfo.getJdbcUrl(), ReflectionUtils.getValue(dataSource, "url"));

View File

@@ -1,21 +1,21 @@
package org.springframework.cloud.service.relational;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.cloud.ReflectionUtils;
import org.springframework.cloud.service.PooledServiceConnectorConfig.PoolConfig;
import org.springframework.cloud.service.ServiceConnectorConfig;
import org.springframework.cloud.service.common.MysqlServiceInfo;
import org.springframework.cloud.service.relational.DataSourceConfig.ConnectionConfig;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import static org.hamcrest.core.IsInstanceOf.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
@@ -108,6 +108,10 @@ public class PooledDataSourceCreatorsTest {
@Test
public void pooledDataSourceCreationInvalid() {
DataSource ds = createMysqlDataSourceWithPooledName("Dummy");
assertThat(ds, instanceOf(UrlDecodingDataSource.class));
ds = ((UrlDecodingDataSource) ds).getTargetDataSource();
assertThat(ds, instanceOf(org.springframework.jdbc.datasource.SimpleDriverDataSource.class));
}
@@ -125,6 +129,10 @@ public class PooledDataSourceCreatorsTest {
}
private void assertBasicDbcpDataSource(DataSource ds) throws ClassNotFoundException {
assertThat(ds, instanceOf(UrlDecodingDataSource.class));
ds = ((UrlDecodingDataSource) ds).getTargetDataSource();
assertThat(ds, instanceOf(Class.forName(DBCP2_BASIC_DATASOURCE)));
assertEquals(MIN_POOL_SIZE, getIntValue(ds, "minIdle"));
@@ -134,6 +142,10 @@ public class PooledDataSourceCreatorsTest {
}
private void assertTomcatDbcpDataSource(DataSource ds) throws ClassNotFoundException {
assertThat(ds, instanceOf(UrlDecodingDataSource.class));
ds = ((UrlDecodingDataSource) ds).getTargetDataSource();
assertTrue(hasClass(TOMCAT_7_DBCP) || hasClass(TOMCAT_8_DBCP));
if (hasClass(TOMCAT_7_DBCP)) {
@@ -154,6 +166,10 @@ public class PooledDataSourceCreatorsTest {
}
private void assertTomcatJdbcDataSource(DataSource ds, boolean overrideConfig) throws ClassNotFoundException {
assertThat(ds, instanceOf(UrlDecodingDataSource.class));
ds = ((UrlDecodingDataSource) ds).getTargetDataSource();
assertThat(ds, instanceOf(Class.forName(TOMCAT_JDBC_DATASOURCE)));
if (overrideConfig) {
@@ -171,6 +187,10 @@ public class PooledDataSourceCreatorsTest {
}
private void assertHikariDataSource(DataSource ds) throws ClassNotFoundException {
assertThat(ds, instanceOf(UrlDecodingDataSource.class));
ds = ((UrlDecodingDataSource) ds).getTargetDataSource();
assertThat(ds, instanceOf(Class.forName(HIKARI_DATASOURCE)));
assertEquals(MIN_POOL_SIZE, getIntValue(ds, "minimumIdle"));

View File

@@ -0,0 +1,204 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.service.relational;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Objects;
import java.util.function.BiFunction;
import javax.sql.DataSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.jdbc.datasource.AbstractDataSource;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class UrlDecodingDataSourceTest {
private static final String ENCODED_URL = "jdbc:mysql://host.example.com:3306/db?user=user%40host&password=password%21";
private static final String DECODED_URL = "jdbc:mysql://host.example.com:3306/db?user=user@host&password=password!";
private static final String URL_PROPERTY_NAME = "url";
private static final String IS_POOLED = "is pooled";
private static final String TRUE = "true";
@Test
public void whenConnectionToEncodedUrlIsSuccessful() throws SQLException {
MockDataSource delegateDataSource = pooledDataSourceRequiringEncodedUrl();
UrlDecodingDataSource urlDecodingDataSource = new UrlDecodingDataSource(delegateDataSource, "url");
Connection connection = urlDecodingDataSource.getConnection();
assertNotNull("Returned connection must not be null", connection);
assertEquals(ENCODED_URL, delegateDataSource.url);
assertEquals("Connection must be made using the pooled (delegate) data source", TRUE, connection.getClientInfo(IS_POOLED));
}
@Test
public void whenConnectionToDecodedUrlIsSuccessful() throws SQLException {
MockDataSource delegateDataSource = pooledDataSourceRequiringDecodedUrl();
UrlDecodingDataSource urlDecodingDataSource = new UrlDecodingDataSource(delegateDataSource, URL_PROPERTY_NAME,
UrlDecodingDataSourceTest::dataSourceRequiringDecodedUrl);
Connection connection = urlDecodingDataSource.getConnection();
assertNotNull("Returned connection must not be null", connection);
assertEquals(DECODED_URL, delegateDataSource.url);
assertEquals("Connection must be made using the pooled (delegate) data source", TRUE, connection.getClientInfo(IS_POOLED));
}
@Test(expected = SQLException.class)
public void whenNoConnectionIsSuccessful() throws SQLException {
MockDataSource delegateDataSource = pooledDataSourceUnableToConnectToAnyUrl();
UrlDecodingDataSource urlDecodingDataSource = new UrlDecodingDataSource(delegateDataSource, URL_PROPERTY_NAME,
UrlDecodingDataSourceTest::dataSourceUnableToConnectToAnyUrl);
try {
urlDecodingDataSource.getConnection();
} catch (SQLException e) {
assertEquals(ENCODED_URL, delegateDataSource.url);
throw e;
}
}
@Test
public void successfulUrlDecodedConnectionTestIsOnlyPerformedOnce() throws SQLException {
MockDataSource delegateDataSource = pooledDataSourceRequiringDecodedUrl();
DataSource decodedUrlConnectionTestDataSource = mock(DataSource.class);
when(decodedUrlConnectionTestDataSource.getConnection()).thenReturn(mock(Connection.class));
UrlDecodingDataSource urlDecodingDataSource = new UrlDecodingDataSource(delegateDataSource, URL_PROPERTY_NAME,
url -> decodedUrlConnectionTestDataSource);
urlDecodingDataSource.getConnection();
urlDecodingDataSource.getConnection();
verify(decodedUrlConnectionTestDataSource, times(1)).getConnection();
}
@Test
public void unsuccessfulUrlDecodedConnectionTestIsTriedAgain() throws SQLException {
MockDataSource delegateDataSource = pooledDataSourceRequiringDecodedUrl();
DataSource decodedUrlConnectionTestDataSource = mock(DataSource.class);
when(decodedUrlConnectionTestDataSource.getConnection()).thenThrow(new SQLException("unable to connect"));
UrlDecodingDataSource urlDecodingDataSource = new UrlDecodingDataSource(delegateDataSource, URL_PROPERTY_NAME,
url -> decodedUrlConnectionTestDataSource);
try {
urlDecodingDataSource.getConnection();
} catch (SQLException e) {}
try {
urlDecodingDataSource.getConnection();
} catch (SQLException e) {}
verify(decodedUrlConnectionTestDataSource, times(2)).getConnection();
}
private static class MockDataSource extends AbstractDataSource {
private final ConnectionSupplier connectionSupplier;
private String url;
MockDataSource(String url, ConnectionSupplier connectionSupplier) {
this.url = url;
this.connectionSupplier = connectionSupplier;
}
@Override
public Connection getConnection() throws SQLException {
return connectionSupplier.getConnection(url);
}
@Override
public Connection getConnection(String username, String password) {
throw new UnsupportedOperationException();
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
@FunctionalInterface
private interface ConnectionSupplier {
Connection getConnection(String url) throws SQLException;
}
private static Connection withPooling(ConnectionSupplier supplier, String url) throws SQLException {
Connection connection = supplier.getConnection(url);
when(connection.getClientInfo(IS_POOLED)).thenReturn(TRUE);
return connection;
}
private static MockDataSource pooledDataSourceUnableToConnectToAnyUrl() {
return new MockDataSource(ENCODED_URL, u -> withPooling(UrlDecodingDataSourceTest::neverConnect, u));
}
private static MockDataSource pooledDataSourceRequiringDecodedUrl() {
return new MockDataSource(ENCODED_URL, u -> withPooling(UrlDecodingDataSourceTest::onlyConnectToDecodedUrls, u));
}
private static MockDataSource pooledDataSourceRequiringEncodedUrl() {
return new MockDataSource(ENCODED_URL, u -> withPooling(UrlDecodingDataSourceTest::onlyConnectToEncodedUrls, u));
}
private static MockDataSource dataSourceUnableToConnectToAnyUrl(String url) {
return new MockDataSource(url, UrlDecodingDataSourceTest::neverConnect);
}
private static MockDataSource dataSourceRequiringDecodedUrl(String url) {
return new MockDataSource(url, UrlDecodingDataSourceTest::onlyConnectToDecodedUrls);
}
private static Connection neverConnect(String url) throws SQLException {
throw new SQLException("unable to connect to " + url);
}
private static Connection onlyConnectToDecodedUrls(String url) throws SQLException {
return onlyConnectToUrlsPassingDecodingTest(url, Objects::equals);
}
private static Connection onlyConnectToEncodedUrls(String url) throws SQLException {
return onlyConnectToUrlsPassingDecodingTest(url, (original, decodedUrl) -> !Objects.equals(original, decodedUrl));
}
private static Connection onlyConnectToUrlsPassingDecodingTest(String url, BiFunction<String, String, Boolean> urlTest) throws SQLException {
try {
String decodedUrl = URLDecoder.decode(url, StandardCharsets.UTF_8.name());
if (urlTest.apply(url, decodedUrl)) {
return mock(Connection.class);
} else {
throw new SQLException("unable to connect to " + url);
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}