diff --git a/spring-boot-devtools/pom.xml b/spring-boot-devtools/pom.xml index db189e46ec..c35f8480f2 100644 --- a/spring-boot-devtools/pom.xml +++ b/spring-boot-devtools/pom.xml @@ -116,6 +116,11 @@ postgresql test + + org.apache.tomcat + tomcat-jdbc + test + org.springframework.boot spring-boot-starter-thymeleaf diff --git a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java index 0e76695220..6e446e38f7 100644 --- a/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java +++ b/spring-boot-devtools/src/main/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfiguration.java @@ -23,18 +23,24 @@ import java.util.Set; import javax.sql.DataSource; import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.AllNestedConditions; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; import org.springframework.boot.autoconfigure.data.jpa.EntityManagerFactoryDependsOnPostProcessor; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.devtools.autoconfigure.DevToolsDataSourceAutoConfiguration.DevToolsDataSourceCondition; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.orm.jpa.AbstractEntityManagerFactoryBean; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; @@ -109,20 +115,37 @@ public class DevToolsDataSourceAutoConfiguration { } - static class DevToolsDataSourceCondition extends AllNestedConditions { - - DevToolsDataSourceCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnBean(DataSource.class) - static final class DataSourceBean { + static class DevToolsDataSourceCondition extends SpringBootCondition + implements ConfigurationCondition { + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; } - @ConditionalOnBean(DataSourceProperties.class) - static final class DataSourcePropertiesBean { - + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + String[] dataSourceBeanNames = context.getBeanFactory() + .getBeanNamesForType(DataSource.class); + if (dataSourceBeanNames.length != 1) { + return ConditionOutcome + .noMatch("A single DataSource bean was not found in the context"); + } + if (context.getBeanFactory() + .getBeanNamesForType(DataSourceProperties.class).length != 1) { + return ConditionOutcome.noMatch( + "A single DataSourceProperties bean was not found in the context"); + } + BeanDefinition dataSourceDefinition = context.getRegistry() + .getBeanDefinition(dataSourceBeanNames[0]); + if (dataSourceDefinition instanceof AnnotatedBeanDefinition + && ((AnnotatedBeanDefinition) dataSourceDefinition) + .getFactoryMethodMetadata().getDeclaringClassName() + .startsWith(DataSourceAutoConfiguration.class.getName())) { + return ConditionOutcome.match("Found auto-configured DataSource"); + } + return ConditionOutcome.noMatch("DataSource was not auto-configured"); } } diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/AbstractDevToolsDataSourceAutoConfigurationTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/AbstractDevToolsDataSourceAutoConfigurationTests.java new file mode 100644 index 0000000000..61d341cb92 --- /dev/null +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/AbstractDevToolsDataSourceAutoConfigurationTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2016 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.boot.devtools.autoconfigure; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Collection; + +import javax.sql.DataSource; + +import org.junit.Test; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Base class for tests for {@link DevToolsDataSourceAutoConfiguration}. + * + * @author Andy Wilkinson + */ +public class AbstractDevToolsDataSourceAutoConfigurationTests { + + @Test + public void singleManuallyConfiguredDataSourceIsNotClosed() throws SQLException { + ConfigurableApplicationContext context = createContext( + DataSourcePropertiesConfiguration.class, + SingleDataSourceConfiguration.class); + DataSource dataSource = context.getBean(DataSource.class); + Statement statement = configureDataSourceBehaviour(dataSource); + verify(statement, times(0)).execute("SHUTDOWN"); + } + + @Test + public void multipleDataSourcesAreIgnored() throws SQLException { + ConfigurableApplicationContext context = createContext( + DataSourcePropertiesConfiguration.class, + MultipleDataSourcesConfiguration.class); + Collection dataSources = context.getBeansOfType(DataSource.class) + .values(); + for (DataSource dataSource : dataSources) { + Statement statement = configureDataSourceBehaviour(dataSource); + verify(statement, times(0)).execute("SHUTDOWN"); + } + } + + protected final Statement configureDataSourceBehaviour(DataSource dataSource) + throws SQLException { + Connection connection = mock(Connection.class); + Statement statement = mock(Statement.class); + doReturn(connection).when(dataSource).getConnection(); + given(connection.createStatement()).willReturn(statement); + return statement; + } + + protected final ConfigurableApplicationContext createContext(String driverClassName, + Class... classes) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(classes); + context.register(DevToolsDataSourceAutoConfiguration.class); + if (driverClassName != null) { + EnvironmentTestUtils.addEnvironment(context, + "spring.datasource.driver-class-name:" + driverClassName); + } + context.refresh(); + return context; + } + + protected final ConfigurableApplicationContext createContext(Class... classes) { + return this.createContext(null, classes); + } + + @Configuration + static class SingleDataSourceConfiguration { + + @Bean + public DataSource dataSource() { + return mock(DataSource.class); + } + + } + + @Configuration + static class MultipleDataSourcesConfiguration { + + @Bean + public DataSource dataSourceOne() { + return mock(DataSource.class); + } + + @Bean + public DataSource dataSourceTwo() { + return mock(DataSource.class); + } + + } + + @Configuration + @EnableConfigurationProperties(DataSourceProperties.class) + static class DataSourcePropertiesConfiguration { + + } + + @Configuration + static class DataSourceSpyConfiguration { + + @Bean + public DataSourceSpyBeanPostProcessor dataSourceSpyBeanPostProcessor() { + return new DataSourceSpyBeanPostProcessor(); + } + + } + + private static class DataSourceSpyBeanPostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + if (bean instanceof DataSource) { + bean = spy(bean); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + } + +} diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfigurationTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfigurationTests.java deleted file mode 100644 index 8e5a711e7c..0000000000 --- a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsDataSourceAutoConfigurationTests.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2012-2016 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.boot.devtools.autoconfigure; - -import java.sql.Connection; -import java.sql.SQLException; -import java.sql.Statement; - -import javax.persistence.EntityManagerFactory; -import javax.sql.DataSource; - -import org.junit.Test; -import org.mockito.InOrder; - -import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.boot.test.EnvironmentTestUtils; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.inOrder; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -/** - * Tests for {@link DevToolsDataSourceAutoConfiguration}. - * - * @author Andy Wilkinson - */ -public class DevToolsDataSourceAutoConfigurationTests { - - @Test - public void embeddedDatabaseIsNotShutDown() throws SQLException { - ConfigurableApplicationContext context = createContextWithDriver("org.h2.Driver", - EmbeddedDatabaseConfiguration.class); - DataSource dataSource = context.getBean(DataSource.class); - context.close(); - verify(dataSource, times(0)).getConnection(); - } - - @Test - public void externalDatabaseIsNotShutDown() throws SQLException { - ConfigurableApplicationContext context = createContextWithDriver( - "org.postgresql.Driver", DataSourceConfiguration.class); - DataSource dataSource = context.getBean(DataSource.class); - context.close(); - verify(dataSource, times(0)).getConnection(); - } - - @Test - public void nonEmbeddedInMemoryDatabaseConfiguredWithDriverIsShutDown() - throws SQLException { - ConfigurableApplicationContext context = createContextWithDriver("org.h2.Driver", - DataSourceConfiguration.class); - DataSource dataSource = context.getBean(DataSource.class); - Connection connection = mock(Connection.class); - given(dataSource.getConnection()).willReturn(connection); - Statement statement = mock(Statement.class); - given(connection.createStatement()).willReturn(statement); - context.close(); - verify(statement).execute("SHUTDOWN"); - } - - @Test - public void nonEmbeddedInMemoryDatabaseConfiguredWithUrlIsShutDown() - throws SQLException { - ConfigurableApplicationContext context = createContextWithUrl("jdbc:h2:mem:test", - DataSourceConfiguration.class); - DataSource dataSource = context.getBean(DataSource.class); - Connection connection = mock(Connection.class); - given(dataSource.getConnection()).willReturn(connection); - Statement statement = mock(Statement.class); - given(connection.createStatement()).willReturn(statement); - context.close(); - verify(statement).execute("SHUTDOWN"); - } - - @Test - public void configurationBacksOffWithoutDataSourceProperties() throws SQLException { - ConfigurableApplicationContext context = createContext("org.h2.Driver", - NoDataSourcePropertiesConfiguration.class); - assertThat( - context.getBeansOfType(DevToolsDataSourceAutoConfiguration.class).size(), - is(0)); - } - - @Test - public void entityManagerFactoryIsClosedBeforeDatabaseIsShutDown() - throws SQLException { - ConfigurableApplicationContext context = createContextWithUrl("jdbc:h2:mem:test", - DataSourceConfiguration.class, EntityManagerFactoryConfiguration.class); - DataSource dataSource = context.getBean(DataSource.class); - Connection connection = mock(Connection.class); - given(dataSource.getConnection()).willReturn(connection); - Statement statement = mock(Statement.class); - given(connection.createStatement()).willReturn(statement); - EntityManagerFactory entityManagerFactory = context - .getBean(EntityManagerFactory.class); - context.close(); - InOrder inOrder = inOrder(statement, entityManagerFactory); - inOrder.verify(statement).execute("SHUTDOWN"); - inOrder.verify(entityManagerFactory).close(); - } - - private ConfigurableApplicationContext createContextWithDriver(String driver, - Class... classes) { - return createContext("spring.datasource.driver-class-name:" + driver, classes); - } - - private ConfigurableApplicationContext createContextWithUrl(String url, - Class... classes) { - return createContext("spring.datasource.url:" + url, classes); - } - - private ConfigurableApplicationContext createContext(String property, - Class... classes) { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - context.register(classes); - context.register(DevToolsDataSourceAutoConfiguration.class); - EnvironmentTestUtils.addEnvironment(context, property); - context.refresh(); - return context; - } - - @Configuration - @EnableConfigurationProperties(DataSourceProperties.class) - static class EmbeddedDatabaseConfiguration { - - @Bean - public EmbeddedDatabase embeddedDatabase() { - return mock(EmbeddedDatabase.class); - } - } - - @Configuration - @EnableConfigurationProperties(DataSourceProperties.class) - static class DataSourceConfiguration { - - @Bean - public DataSource dataSource() { - return mock(DataSource.class); - } - - } - - @Configuration - static class NoDataSourcePropertiesConfiguration { - - @Bean - public DataSource dataSource() { - return mock(DataSource.class); - } - - } - - @Configuration - static class EntityManagerFactoryConfiguration { - - @Bean - public EntityManagerFactory entityManagerFactory() { - return mock(EntityManagerFactory.class); - } - - } - -} diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsEmbeddedDataSourceAutoConfigurationTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsEmbeddedDataSourceAutoConfigurationTests.java new file mode 100644 index 0000000000..107af415d8 --- /dev/null +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsEmbeddedDataSourceAutoConfigurationTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2016 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.boot.devtools.autoconfigure; + +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.devtools.test.ClassPathExclusions; +import org.springframework.boot.devtools.test.FilteredClassPathRunner; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link DevToolsDataSourceAutoConfiguration} with an embedded data source. + * + * @author Andy Wilkinson + */ +@RunWith(FilteredClassPathRunner.class) +@ClassPathExclusions("tomcat-jdbc-*.jar") +public class DevToolsEmbeddedDataSourceAutoConfigurationTests + extends AbstractDevToolsDataSourceAutoConfigurationTests { + + @Test + public void autoConfiguredDataSourceIsNotShutdown() throws SQLException { + ConfigurableApplicationContext context = createContext( + DataSourceAutoConfiguration.class, DataSourceSpyConfiguration.class); + Statement statement = configureDataSourceBehaviour( + context.getBean(DataSource.class)); + context.close(); + verify(statement, times(0)).execute("SHUTDOWN"); + } + +} diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsPooledDataSourceAutoConfigurationTests.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsPooledDataSourceAutoConfigurationTests.java new file mode 100644 index 0000000000..050ae6308f --- /dev/null +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/autoconfigure/DevToolsPooledDataSourceAutoConfigurationTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2012-2016 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.boot.devtools.autoconfigure; + +import java.sql.SQLException; +import java.sql.Statement; + +import javax.sql.DataSource; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.devtools.test.FilteredClassPathRunner; +import org.springframework.context.ConfigurableApplicationContext; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link DevToolsDataSourceAutoConfiguration} with a pooled data source. + * + * @author Andy Wilkinson + */ +@RunWith(FilteredClassPathRunner.class) +public class DevToolsPooledDataSourceAutoConfigurationTests + extends AbstractDevToolsDataSourceAutoConfigurationTests { + + @Test + public void autoConfiguredInMemoryDataSourceIsShutdown() throws SQLException { + ConfigurableApplicationContext context = createContext( + DataSourceAutoConfiguration.class, DataSourceSpyConfiguration.class); + Statement statement = configureDataSourceBehaviour( + context.getBean(DataSource.class)); + context.close(); + verify(statement).execute("SHUTDOWN"); + } + + @Test + public void autoConfiguredExternalDataSourceIsNotShutdown() throws SQLException { + ConfigurableApplicationContext context = createContext("org.postgresql.Driver", + DataSourceAutoConfiguration.class, DataSourceSpyConfiguration.class); + Statement statement = configureDataSourceBehaviour( + context.getBean(DataSource.class)); + context.close(); + verify(statement, times(0)).execute("SHUTDOWN"); + } + +} diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/ClassPathExclusions.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/ClassPathExclusions.java new file mode 100644 index 0000000000..a6dafff28a --- /dev/null +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/ClassPathExclusions.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2016 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.boot.devtools.test; + +import java.io.File; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used in combination with {@link FilteredClassPathRunner} to exclude entries + * from the classpath. + * + * @author Andy Wilkinson + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ClassPathExclusions { + + /** + * One or more Ant-style patterns that identify entries to be excluded from the class + * path. Matching is performed against an entry's {@link File#getName() file name}. + * For example, to exclude Hibernate Validator from the classpath, + * {@code "hibernate-validator-*.jar"} can be used. + * @return the exclusion patterns + */ + String[] value(); + +} diff --git a/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/FilteredClassPathRunner.java b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/FilteredClassPathRunner.java new file mode 100644 index 0000000000..9e9e3839af --- /dev/null +++ b/spring-boot-devtools/src/test/java/org/springframework/boot/devtools/test/FilteredClassPathRunner.java @@ -0,0 +1,222 @@ +/* + * Copyright 2012-2016 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.boot.devtools.test; + +import java.io.File; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.InitializationError; +import org.junit.runners.model.TestClass; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.StringUtils; + +/** + * A custom {@link BlockJUnit4ClassRunner} that runs tests using a filtered class path. + * Entries are excluded from the class path using {@link ClassPathExclusions} on the test + * class. A class loader is created with the customized class path and is used both to + * load the test class and as the thread context class loader while the test is being run. + * + * @author Andy Wilkinson + */ +public class FilteredClassPathRunner extends BlockJUnit4ClassRunner { + + public FilteredClassPathRunner(Class testClass) throws InitializationError { + super(testClass); + } + + @Override + protected TestClass createTestClass(Class testClass) { + try { + ClassLoader classLoader = createTestClassLoader(testClass); + return new FilteredTestClass(classLoader, testClass.getName()); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + private URLClassLoader createTestClassLoader(Class testClass) throws Exception { + URLClassLoader classLoader = (URLClassLoader) this.getClass().getClassLoader(); + return new URLClassLoader(filterUrls(extractUrls(classLoader), testClass), + classLoader.getParent()); + } + + private URL[] extractUrls(URLClassLoader classLoader) throws Exception { + List extractedUrls = new ArrayList(); + for (URL url : classLoader.getURLs()) { + if (isSurefireBooterJar(url)) { + extractedUrls.addAll(extractUrlsFromManifestClassPath(url)); + } + else { + extractedUrls.add(url); + } + } + return extractedUrls.toArray(new URL[extractedUrls.size()]); + } + + private boolean isSurefireBooterJar(URL url) { + return url.getPath().contains("surefirebooter"); + } + + private List extractUrlsFromManifestClassPath(URL booterJar) throws Exception { + List urls = new ArrayList(); + for (String entry : getClassPath(booterJar)) { + urls.add(new URL(entry)); + } + return urls; + } + + private String[] getClassPath(URL booterJar) throws Exception { + JarFile jarFile = new JarFile(new File(booterJar.toURI())); + try { + return StringUtils.delimitedListToStringArray(jarFile.getManifest() + .getMainAttributes().getValue(Attributes.Name.CLASS_PATH), " "); + } + finally { + jarFile.close(); + } + } + + private URL[] filterUrls(URL[] urls, Class testClass) throws Exception { + ClassPathEntryFilter filter = new ClassPathEntryFilter(testClass); + List filteredUrls = new ArrayList(); + for (URL url : urls) { + if (!filter.isExcluded(url)) { + filteredUrls.add(url); + } + } + return filteredUrls.toArray(new URL[filteredUrls.size()]); + } + + /** + * Filter for class path entries. + */ + private static final class ClassPathEntryFilter { + + private final List exclusions; + + private final AntPathMatcher matcher = new AntPathMatcher(); + + private ClassPathEntryFilter(Class testClass) throws Exception { + ClassPathExclusions exclusions = AnnotationUtils.findAnnotation(testClass, + ClassPathExclusions.class); + this.exclusions = exclusions == null ? Collections.emptyList() + : Arrays.asList(exclusions.value()); + } + + private boolean isExcluded(URL url) throws Exception { + if (!"file".equals(url.getProtocol())) { + return false; + } + String name = new File(url.toURI()).getName(); + for (String exclusion : this.exclusions) { + if (this.matcher.match(exclusion, name)) { + return true; + } + } + return false; + } + } + + /** + * Filtered version of JUnit's {@link TestClass}. + */ + private static final class FilteredTestClass extends TestClass { + + private final ClassLoader classLoader; + + FilteredTestClass(ClassLoader classLoader, String testClassName) + throws ClassNotFoundException { + super(classLoader.loadClass(testClassName)); + this.classLoader = classLoader; + } + + @Override + public List getAnnotatedMethods( + Class annotationClass) { + try { + return getAnnotatedMethods(annotationClass.getName()); + } + catch (ClassNotFoundException ex) { + throw new RuntimeException(ex); + } + } + + @SuppressWarnings("unchecked") + private List getAnnotatedMethods(String annotationClassName) + throws ClassNotFoundException { + Class annotationClass = (Class) this.classLoader + .loadClass(annotationClassName); + List methods = super.getAnnotatedMethods(annotationClass); + return wrapFrameworkMethods(methods); + } + + private List wrapFrameworkMethods( + List methods) { + List wrapped = new ArrayList( + methods.size()); + for (FrameworkMethod frameworkMethod : methods) { + wrapped.add(new FilteredFrameworkMethod(this.classLoader, + frameworkMethod.getMethod())); + } + return wrapped; + } + + } + + /** + * Filtered version of JUnit's {@link FrameworkMethod}. + */ + private static final class FilteredFrameworkMethod extends FrameworkMethod { + + private final ClassLoader classLoader; + + private FilteredFrameworkMethod(ClassLoader classLoader, Method method) { + super(method); + this.classLoader = classLoader; + } + + @Override + public Object invokeExplosively(Object target, Object... params) + throws Throwable { + ClassLoader originalClassLoader = Thread.currentThread() + .getContextClassLoader(); + Thread.currentThread().setContextClassLoader(this.classLoader); + try { + return super.invokeExplosively(target, params); + } + finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } + + } + +}