From 56fd4e1f76f3df9dcc15482813cce1b48fd4e723 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 22 Mar 2016 11:42:26 -0700 Subject: [PATCH] Add TypeExcludeFilters support Provide a way for auto-configuration tests to easily filter scanned components. See gh-4901 --- ...notationCustomizableTypeExcludeFilter.java | 110 +++++++++++++ .../filter/FilterAnnotations.java | 114 +++++++++++++ .../filter/TypeExcludeFilters.java | 51 ++++++ .../TypeExcludeFiltersContextCustomizer.java | 119 ++++++++++++++ ...xcludeFiltersContextCustomizerFactory.java | 52 ++++++ .../autoconfigure/filter/package-info.java | 21 +++ .../main/resources/META-INF/spring.factories | 1 + .../filter/FilterAnnotationsTests.java | 152 ++++++++++++++++++ ...tersTestContextCustomizerFactoryTests.java | 135 ++++++++++++++++ 9 files changed, 755 insertions(+) create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/FilterAnnotations.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFilters.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizer.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactory.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/package-info.java create mode 100644 spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/FilterAnnotationsTests.java create mode 100644 spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersTestContextCustomizerFactoryTests.java diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java new file mode 100644 index 00000000000..f5e72f50225 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/AnnotationCustomizableTypeExcludeFilter.java @@ -0,0 +1,110 @@ +/* + * 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.test.autoconfigure.filter; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.Set; + +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; + +/** + * Abstract base class for a {@link TypeExcludeFilter} that can be customized using an + * annotation. + * + * @author Phillip Webb + * @since 1.4.0 + */ +public abstract class AnnotationCustomizableTypeExcludeFilter extends TypeExcludeFilter + implements BeanClassLoaderAware { + + private ClassLoader classLoader; + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public boolean match(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) throws IOException { + if (hasAnnotation()) { + return !(include(metadataReader, metadataReaderFactory) + && !exclude(metadataReader, metadataReaderFactory)); + } + return false; + } + + protected boolean include(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) throws IOException { + if (new FilterAnnotations(this.classLoader, getFilters(FilterType.INCLUDE)) + .anyMatches(metadataReader, metadataReaderFactory)) { + return true; + } + if (isUseDefaultFilters() + && defaultInclude(metadataReader, metadataReaderFactory)) { + return true; + } + return false; + } + + protected boolean defaultInclude(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) throws IOException { + for (Class include : getDefaultIncudes()) { + if (isTypeOrAnnotated(metadataReader, metadataReaderFactory, include)) { + return true; + } + } + return false; + } + + protected boolean exclude(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) throws IOException { + return new FilterAnnotations(this.classLoader, getFilters(FilterType.EXCLUDE)) + .anyMatches(metadataReader, metadataReaderFactory); + } + + @SuppressWarnings("unchecked") + protected final boolean isTypeOrAnnotated(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory, Class type) + throws IOException { + AnnotationTypeFilter annotationFilter = new AnnotationTypeFilter( + (Class) type); + AssignableTypeFilter typeFilter = new AssignableTypeFilter(type); + return annotationFilter.match(metadataReader, metadataReaderFactory) + || typeFilter.match(metadataReader, metadataReaderFactory); + } + + protected abstract boolean hasAnnotation(); + + protected abstract Filter[] getFilters(FilterType type); + + protected abstract boolean isUseDefaultFilters(); + + protected abstract Set> getDefaultIncudes(); + + protected static enum FilterType { + INCLUDE, EXCLUDE + }; + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/FilterAnnotations.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/FilterAnnotations.java new file mode 100644 index 00000000000..fa5da394676 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/FilterAnnotations.java @@ -0,0 +1,114 @@ +/* + * 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.test.autoconfigure.filter; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Pattern; + +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AspectJTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.RegexPatternTypeFilter; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.util.Assert; + +/** + * Utility to load {@link TypeFilter TypeFilters} from {@link Filter @Filter} annotations. + * + * @author Phillip Webb + * @since 1.4.0 + */ +public class FilterAnnotations implements Iterable { + + private final ClassLoader classLoader; + + private final List filters; + + public FilterAnnotations(ClassLoader classLoader, Filter[] filters) { + Assert.notNull(filters, "Filters must not be null"); + this.classLoader = classLoader; + this.filters = createTypeFilters(filters); + } + + private List createTypeFilters(Filter[] filters) { + List typeFilters = new ArrayList(); + for (Filter filter : filters) { + for (Class filterClass : filter.classes()) { + typeFilters.add(createTypeFilter(filter.type(), filterClass)); + } + for (String pattern : filter.pattern()) { + typeFilters.add(createTypeFilter(filter.type(), pattern)); + } + } + return Collections.unmodifiableList(typeFilters); + } + + @SuppressWarnings("unchecked") + private TypeFilter createTypeFilter(FilterType filterType, Class filterClass) { + switch (filterType) { + case ANNOTATION: + Assert.isAssignable(Annotation.class, filterClass, + "An error occured while processing a ANNOTATION type filter: "); + return new AnnotationTypeFilter((Class) filterClass); + case ASSIGNABLE_TYPE: + return new AssignableTypeFilter(filterClass); + case CUSTOM: + Assert.isAssignable(TypeFilter.class, filterClass, + "An error occured while processing a CUSTOM type filter: "); + return BeanUtils.instantiateClass(filterClass, TypeFilter.class); + } + throw new IllegalArgumentException( + "Filter type not supported with Class value: " + filterType); + } + + private TypeFilter createTypeFilter(FilterType filterType, String pattern) { + switch (filterType) { + case ASPECTJ: + return new AspectJTypeFilter(pattern, this.classLoader); + case REGEX: + return new RegexPatternTypeFilter(Pattern.compile(pattern)); + } + throw new IllegalArgumentException( + "Filter type not supported with String pattern: " + filterType); + } + + @Override + public Iterator iterator() { + return this.filters.iterator(); + } + + public boolean anyMatches(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) throws IOException { + for (TypeFilter filter : this) { + if (filter.match(metadataReader, metadataReaderFactory)) { + return true; + } + } + return false; + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFilters.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFilters.java new file mode 100644 index 00000000000..40d7270a6ed --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFilters.java @@ -0,0 +1,51 @@ +/* + * 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.test.autoconfigure.filter; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.TypeExcludeFilter; + +/** + * Annotation that can on tests to define a set of {@link TypeExcludeFilter } classes that + * should be applied to {@link SpringBootApplication @SpringBootApplication} component + * scanning. + * + * @author Phillip Webb + * @since 1.4.0 + * @see TypeExcludeFilter + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TypeExcludeFilters { + + /** + * Specifies {@link TypeExcludeFilter} classes that should be applied to + * {@link SpringBootApplication @SpringBootApplication} component scanning. Classes + * specified here can either have a no-arg constructor or accept a single + * {@code Class} argument if then need access to the {@code testClass}. + * @see TypeExcludeFilter + */ + Class[] value(); + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizer.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizer.java new file mode 100644 index 00000000000..c386a22a3bc --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizer.java @@ -0,0 +1,119 @@ +/* + * 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.test.autoconfigure.filter; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.ReflectionUtils; + +/** + * {@link ContextCustomizer} to support {@link TypeExcludeFilters @TypeExcludeFilters}. + * + * @author Phillip Webb + * @see TypeExcludeFilters + */ +class TypeExcludeFiltersContextCustomizer implements ContextCustomizer { + + private static final String EXCLUDE_FILTER_BEAN_NAME = TypeExcludeFilters.class + .getName(); + + private final Class testClass; + + private final Set> filterClasses; + + TypeExcludeFiltersContextCustomizer(Class testClass, + Set> filterClasses) { + this.testClass = testClass; + this.filterClasses = filterClasses; + } + + @Override + public int hashCode() { + return this.filterClasses.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj != null && getClass().equals(obj.getClass()) && this.filterClasses + .equals(((TypeExcludeFiltersContextCustomizer) obj).filterClasses)); + } + + @Override + public void customizeContext(ConfigurableApplicationContext context, + MergedContextConfiguration mergedContextConfiguration) { + if (!this.filterClasses.isEmpty()) { + context.getBeanFactory().registerSingleton(EXCLUDE_FILTER_BEAN_NAME, + createDelegatingTypeExcludeFilter()); + } + } + + private TypeExcludeFilter createDelegatingTypeExcludeFilter() { + final Set filters = new LinkedHashSet( + this.filterClasses.size()); + for (Class filterClass : this.filterClasses) { + filters.add(createTypeExcludeFilter(filterClass)); + } + return new TypeExcludeFilter() { + + @Override + public boolean match(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) throws IOException { + for (TypeExcludeFilter filter : filters) { + if (filter.match(metadataReader, metadataReaderFactory)) { + return true; + } + } + return false; + } + + }; + } + + private TypeExcludeFilter createTypeExcludeFilter(Class type) { + try { + Constructor constructor = getTypeExcludeFilterConstructor(type); + ReflectionUtils.makeAccessible(constructor); + if (constructor.getParameterTypes().length == 1) { + return (TypeExcludeFilter) constructor.newInstance(this.testClass); + } + return (TypeExcludeFilter) constructor.newInstance(); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to create filter for " + type, ex); + } + } + + private Constructor getTypeExcludeFilterConstructor(Class type) + throws NoSuchMethodException { + try { + return type.getDeclaredConstructor(Class.class); + } + catch (Exception ex) { + return type.getDeclaredConstructor(); + } + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactory.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactory.java new file mode 100644 index 00000000000..80481ac27a5 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersContextCustomizerFactory.java @@ -0,0 +1,52 @@ +/* + * 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.test.autoconfigure.filter; + +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; + +/** + * {@link ContextCustomizerFactory} to support + * {@link TypeExcludeFilters @TypeExcludeFilters}. + * + * @author Phillip Webb + * @see TypeExcludeFiltersContextCustomizer + */ +class TypeExcludeFiltersContextCustomizerFactory implements ContextCustomizerFactory { + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, + List configurationAttributes) { + TypeExcludeFilters annotation = AnnotatedElementUtils + .findMergedAnnotation(testClass, TypeExcludeFilters.class); + if (annotation != null) { + Set> filterClasses = new LinkedHashSet>( + Arrays.asList(annotation.value())); + return new TypeExcludeFiltersContextCustomizer(testClass, filterClasses); + } + return null; + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/package-info.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/package-info.java new file mode 100644 index 00000000000..a4bcbab821c --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/filter/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Helper utilities for using {@link org.springframework.boot.context.TypeExcludeFilter} + * with auto-configured tests. + */ +package org.springframework.boot.test.autoconfigure.filter; diff --git a/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories index 04270057b52..9689cc16ace 100644 --- a/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-test-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,6 +1,7 @@ # Spring Test ContextCustomizerFactories org.springframework.test.context.ContextCustomizerFactory=\ org.springframework.boot.test.autoconfigure.OverrideAutoConfigurationContextCustomizerFactory,\ +org.springframework.boot.test.autoconfigure.filter.TypeExcludeFiltersContextCustomizerFactory,\ org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizerFactory # Test Execution Listeners diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/FilterAnnotationsTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/FilterAnnotationsTests.java new file mode 100644 index 00000000000..3743e2c7db2 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/FilterAnnotationsTests.java @@ -0,0 +1,152 @@ +/* + * 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.test.autoconfigure.filter; + +import java.io.IOException; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.Test; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.FilterType; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; +import org.springframework.stereotype.Service; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link FilterAnnotations}. + * + * @author Phillip Webb + */ +public class FilterAnnotationsTests { + + @Test + public void filterAnnotation() throws Exception { + FilterAnnotations filterAnnotations = get(FilterByAnnotation.class); + assertThat(match(filterAnnotations, ExampleWithAnnotation.class)).isTrue(); + assertThat(match(filterAnnotations, ExampleWithoutAnnotation.class)).isFalse(); + } + + @Test + public void filterAssignableType() throws Exception { + FilterAnnotations filterAnnotations = get(FilterByType.class); + assertThat(match(filterAnnotations, ExampleWithAnnotation.class)).isFalse(); + assertThat(match(filterAnnotations, ExampleWithoutAnnotation.class)).isTrue(); + } + + @Test + public void filterCustom() throws Exception { + FilterAnnotations filterAnnotations = get(FilterByCustom.class); + assertThat(match(filterAnnotations, ExampleWithAnnotation.class)).isFalse(); + assertThat(match(filterAnnotations, ExampleWithoutAnnotation.class)).isTrue(); + } + + @Test + public void filterAspectJ() throws Exception { + FilterAnnotations filterAnnotations = get(FilterByAspectJ.class); + assertThat(match(filterAnnotations, ExampleWithAnnotation.class)).isFalse(); + assertThat(match(filterAnnotations, ExampleWithoutAnnotation.class)).isTrue(); + } + + @Test + public void filterRegex() throws Exception { + FilterAnnotations filterAnnotations = get(FilterByRegex.class); + assertThat(match(filterAnnotations, ExampleWithAnnotation.class)).isFalse(); + assertThat(match(filterAnnotations, ExampleWithoutAnnotation.class)).isTrue(); + } + + @Test + public void anyMatches() throws Exception { + } + + private FilterAnnotations get(Class type) { + Filters filters = AnnotatedElementUtils.getMergedAnnotation(type, Filters.class); + return new FilterAnnotations(getClass().getClassLoader(), filters.value()); + } + + private boolean match(FilterAnnotations filterAnnotations, Class type) + throws IOException { + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory + .getMetadataReader(type.getName()); + return filterAnnotations.anyMatches(metadataReader, metadataReaderFactory); + } + + @Filters(@Filter(Service.class)) + static class FilterByAnnotation { + + } + + @Filters(@Filter(type = FilterType.ASSIGNABLE_TYPE, classes = ExampleWithoutAnnotation.class)) + static class FilterByType { + + } + + @Filters(@Filter(type = FilterType.CUSTOM, classes = ExampleCustomFilter.class)) + static class FilterByCustom { + + } + + @Filters(@Filter(type = FilterType.ASPECTJ, pattern = "(*..*ExampleWithoutAnnotation)")) + static class FilterByAspectJ { + + } + + @Filters(@Filter(type = FilterType.REGEX, pattern = ".*ExampleWithoutAnnotation")) + static class FilterByRegex { + + } + + @Target({ ElementType.TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + static @interface Filters { + + Filter[] value(); + + } + + static class ExampleCustomFilter implements TypeFilter { + + @Override + public boolean match(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) throws IOException { + return metadataReader.getClassMetadata().getClassName() + .equals(ExampleWithoutAnnotation.class.getName()); + } + + } + + @Service + static class ExampleWithAnnotation { + + } + + static class ExampleWithoutAnnotation { + + } + +} diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersTestContextCustomizerFactoryTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersTestContextCustomizerFactoryTests.java new file mode 100644 index 00000000000..f357ebb4c0b --- /dev/null +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/filter/TypeExcludeFiltersTestContextCustomizerFactoryTests.java @@ -0,0 +1,135 @@ +/* + * 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.test.autoconfigure.filter; + +import java.io.IOException; + +import org.junit.Test; + +import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link TypeExcludeFiltersContextCustomizerFactory}. + * + * @author Phillip Webb + */ +public class TypeExcludeFiltersTestContextCustomizerFactoryTests { + + private TypeExcludeFiltersContextCustomizerFactory factory = new TypeExcludeFiltersContextCustomizerFactory(); + + private MergedContextConfiguration mergedContextConfiguration = mock( + MergedContextConfiguration.class); + + private ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(); + + @Test + public void getContextCustomizerWhenHasNoAnnotationShouldReturnNull() + throws Exception { + ContextCustomizer customizer = this.factory + .createContextCustomizer(NoAnnotation.class, null); + assertThat(customizer).isNull(); + } + + @Test + public void getContextCustomizerWhenHasAnnotationShouldReturnCustomizer() + throws Exception { + ContextCustomizer customizer = this.factory + .createContextCustomizer(WithExcludeFilters.class, null); + assertThat(customizer).isNotNull(); + } + + @Test + public void hashCodeAndEquals() throws Exception { + ContextCustomizer customizer1 = this.factory + .createContextCustomizer(WithExcludeFilters.class, null); + ContextCustomizer customizer2 = this.factory + .createContextCustomizer(WithSameExcludeFilters.class, null); + ContextCustomizer customizer3 = this.factory + .createContextCustomizer(WithDifferentExcludeFilters.class, null); + assertThat(customizer1.hashCode()).isEqualTo(customizer2.hashCode()); + assertThat(customizer1).isEqualTo(customizer1).isEqualTo(customizer2) + .isNotEqualTo(customizer3); + } + + @Test + public void getContextCustomizerShouldAddExcludeFilters() throws Exception { + ContextCustomizer customizer = this.factory + .createContextCustomizer(WithExcludeFilters.class, null); + customizer.customizeContext(this.context, this.mergedContextConfiguration); + this.context.refresh(); + TypeExcludeFilter filter = this.context.getBean(TypeExcludeFilter.class); + MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + MetadataReader metadataReader = metadataReaderFactory + .getMetadataReader(NoAnnotation.class.getName()); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isFalse(); + metadataReader = metadataReaderFactory + .getMetadataReader(SimpleExclude.class.getName()); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue(); + metadataReader = metadataReaderFactory + .getMetadataReader(TestClassAwareExclude.class.getName()); + assertThat(filter.match(metadataReader, metadataReaderFactory)).isTrue(); + } + + static class NoAnnotation { + + } + + @TypeExcludeFilters({ SimpleExclude.class, TestClassAwareExclude.class }) + static class WithExcludeFilters { + + } + + @TypeExcludeFilters({ TestClassAwareExclude.class, SimpleExclude.class }) + static class WithSameExcludeFilters { + + } + + @TypeExcludeFilters(SimpleExclude.class) + static class WithDifferentExcludeFilters { + + } + + static class SimpleExclude extends TypeExcludeFilter { + + @Override + public boolean match(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) throws IOException { + return metadataReader.getClassMetadata().getClassName() + .equals(getClass().getName()); + } + + } + + static class TestClassAwareExclude extends SimpleExclude { + + TestClassAwareExclude(Class testClass) { + assertThat(testClass).isEqualTo(WithExcludeFilters.class); + } + + } + +}