From 46099c753b1a2bfcc53d11899091735e632203c4 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 22 Mar 2016 11:42:08 -0700 Subject: [PATCH] Add @PropertyMapping support Add @PropertyMapping annotation which can be used to mark annotation attributes that should contribute Environment properties. Provides a quick way for tests to change auto-configuration behavior in a structured way. Fixes gh-4901 --- .../properties/AnnotationsPropertySource.java | 174 ++++++++++++ .../properties/PropertyMapping.java | 71 +++++ .../PropertyMappingContextCustomizer.java | 94 +++++++ ...opertyMappingContextCustomizerFactory.java | 42 +++ .../properties/package-info.java | 20 ++ .../main/resources/META-INF/spring.factories | 3 +- .../AnnotationsPropertySourceTests.java | 265 ++++++++++++++++++ .../properties/ExampleMapping.java | 33 +++ ...yMappingContextCustomizerFactoryTests.java | 158 +++++++++++ .../properties/PropertyMappingTests.java | 45 +++ 10 files changed, 904 insertions(+), 1 deletion(-) create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMapping.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizer.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactory.java create mode 100644 spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/package-info.java create mode 100644 spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java create mode 100644 spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/ExampleMapping.java create mode 100644 spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactoryTests.java create mode 100644 spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingTests.java diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java new file mode 100644 index 0000000000..02f6f905e8 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySource.java @@ -0,0 +1,174 @@ +/* + * 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.properties; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.EnumerablePropertySource; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * {@link EnumerablePropertySource} to adapt annotations marked with + * {@link PropertyMapping @PropertyMapping}. + * + * @author Phillip Webb + * @since 1.4.0 + */ +public class AnnotationsPropertySource extends EnumerablePropertySource> { + + private static final Pattern CAMEL_CASE_PATTERN = Pattern.compile("([^A-Z-])([A-Z])"); + + private final Map properties; + + public AnnotationsPropertySource(Class source) { + this("Annotations", source); + } + + public AnnotationsPropertySource(String name, Class source) { + super(name, source); + this.properties = getProperties(source); + } + + private Map getProperties(Class source) { + Map properties = new LinkedHashMap(); + collectProperties(source, properties); + return Collections.unmodifiableMap(properties); + } + + private void collectProperties(Class source, Map properties) { + if (source != null) { + for (Annotation annotation : source.getDeclaredAnnotations()) { + if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) { + PropertyMapping typeMapping = AnnotationUtils.getAnnotation( + annotation.annotationType(), PropertyMapping.class); + for (Method attribute : annotation.annotationType() + .getDeclaredMethods()) { + collectProperties(annotation, attribute, typeMapping, properties); + } + } + } + collectProperties(source.getSuperclass(), properties); + } + } + + private void collectProperties(Annotation annotation, Method attribute, + PropertyMapping typeMapping, Map properties) { + PropertyMapping attributeMapping = AnnotationUtils.getAnnotation(attribute, + PropertyMapping.class); + if (isMapped(typeMapping, attributeMapping)) { + String name = getName(typeMapping, attributeMapping, attribute); + ReflectionUtils.makeAccessible(attribute); + Object value = ReflectionUtils.invokeMethod(attribute, annotation); + putProperties(name, value, properties); + } + } + + private boolean isMapped(PropertyMapping typeMapping, + PropertyMapping attributeMapping) { + if (attributeMapping != null) { + return attributeMapping.map(); + } + return (typeMapping != null && typeMapping.map()); + } + + private String getName(PropertyMapping typeMapping, PropertyMapping attributeMapping, + Method attribute) { + String prefix = (typeMapping == null ? "" : typeMapping.value()); + String name = (attributeMapping == null ? "" : attributeMapping.value()); + if (!StringUtils.hasText(name)) { + name = toKebabCase(attribute.getName()); + } + return dotAppend(prefix, name); + } + + private String toKebabCase(String name) { + Matcher matcher = CAMEL_CASE_PATTERN.matcher(name); + StringBuffer result = new StringBuffer(); + while (matcher.find()) { + matcher.appendReplacement(result, + matcher.group(1) + '-' + StringUtils.uncapitalize(matcher.group(2))); + } + matcher.appendTail(result); + return result.toString().toLowerCase(); + } + + private String dotAppend(String prefix, String postfix) { + prefix = (prefix == null ? "" : prefix); + if (StringUtils.hasText(prefix)) { + return (prefix.endsWith(".") ? prefix + postfix : prefix + "." + postfix); + } + return postfix; + } + + private void putProperties(String name, Object value, + Map properties) { + if (ObjectUtils.isArray(value)) { + Object[] array = ObjectUtils.toObjectArray(value); + for (int i = 0; i < array.length; i++) { + properties.put(name + "[" + i + "]", array[i]); + } + } + else { + properties.put(name, value); + } + } + + @Override + public boolean containsProperty(String name) { + return this.properties.containsKey(name); + } + + @Override + public Object getProperty(String name) { + return this.properties.get(name); + } + + @Override + public String[] getPropertyNames() { + return StringUtils.toStringArray(this.properties.keySet()); + } + + public boolean isEmpty() { + return this.properties.isEmpty(); + } + + @Override + public int hashCode() { + return this.properties.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return this.properties.equals(((AnnotationsPropertySource) obj).properties); + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMapping.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMapping.java new file mode 100644 index 0000000000..2796fd4056 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMapping.java @@ -0,0 +1,71 @@ +/* + * 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.properties; + +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.context.annotation.PropertySource; +import org.springframework.test.context.TestPropertySource; + +/** + * Indicates that attributes from a test annotation should be mapped into a + * {@link PropertySource}. Can be used at the type level, or on individual attributes. For + * example, the following annotation declaration:
+ * @Retention(RUNTIME)
+ * @PropertyMapping("my.example")
+ * public @interface Example {
+ *
+ *   String name();
+ *
+ * }
+ * 
When used on a test class as follows:
+ * @Example(name="Spring")
+ * public class MyTest {
+ * }
+ * 
will result in a {@literal my.example.name} property being added with the value + * {@literal "Spring"}. + *

+ * + * @author Phillip Webb + * @since 1.4.0 + * @see AnnotationsPropertySource + * @see TestPropertySource + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface PropertyMapping { + + /** + * Defines the property mapping. When used at the type-level, this value will be used + * as a prefix for all mapped attributes. When used on an attribute, the value + * overrides the generated (kebab case) name. + */ + String value() default ""; + + /** + * Determines if mapping should occur. When specified at the type-level indicates if + * mapping should occur by default or not. When used at the attribute-level, overrides + * the type-level default. + */ + boolean map() default true; + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizer.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizer.java new file mode 100644 index 0000000000..f57a1478e1 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizer.java @@ -0,0 +1,94 @@ +/* + * 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.properties; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.util.Assert; + +/** + * {@link ContextCustomizer} to map annotation attributes to {@link Environment} + * properties. + * + * @author Phillip Webb + */ +class PropertyMappingContextCustomizer implements ContextCustomizer { + + private final AnnotationsPropertySource propertySource; + + PropertyMappingContextCustomizer(AnnotationsPropertySource propertySource) { + this.propertySource = propertySource; + } + + @Override + public void customizeContext(ConfigurableApplicationContext context, + MergedContextConfiguration mergedContextConfiguration) { + if (!this.propertySource.isEmpty()) { + context.getEnvironment().getPropertySources().addFirst(this.propertySource); + } + context.getBeanFactory().registerSingleton( + PropertyMappingCheckBeanPostProcessor.class.getName(), + new PropertyMappingCheckBeanPostProcessor()); + } + + @Override + public int hashCode() { + return this.propertySource.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj != null && getClass().equals(obj.getClass()) && this.propertySource + .equals(((PropertyMappingContextCustomizer) obj).propertySource)); + } + + /** + * {@link BeanPostProcessor} to check that {@link PropertyMapping} is only used on + * test classes. + */ + static class PropertyMappingCheckBeanPostProcessor implements BeanPostProcessor { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + Class beanClass = bean.getClass(); + boolean hasComponent = AnnotationUtils.findAnnotation(beanClass, + Component.class) != null; + boolean hasPropertyMapping = AnnotationUtils.findAnnotation(beanClass, + PropertyMapping.class) != null; + if (hasComponent) { + Assert.state(!hasPropertyMapping, + "@PropertyMapping annotations can only be used on test classes"); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactory.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactory.java new file mode 100644 index 0000000000..97d5e71848 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactory.java @@ -0,0 +1,42 @@ +/* + * 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.properties; + +import java.util.List; + +import org.springframework.core.env.Environment; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; + +/** + * {@link ContextCustomizerFactory} to map annotation attributes to {@link Environment} + * properties. + * + * @author Phillip Webb + */ +class PropertyMappingContextCustomizerFactory implements ContextCustomizerFactory { + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, + List configurationAttributes) { + AnnotationsPropertySource propertySource = new AnnotationsPropertySource( + testClass); + return new PropertyMappingContextCustomizer(propertySource); + } + +} diff --git a/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/package-info.java b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/package-info.java new file mode 100644 index 0000000000..47b60a8806 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/properties/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Support for mapping annotation attribute values in the Spring {@code Environment}. + */ +package org.springframework.boot.test.autoconfigure.properties; 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 b8b5a32531..04270057b5 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.OverrideAutoConfigurationContextCustomizerFactory,\ +org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizerFactory # Test Execution Listeners org.springframework.test.context.TestExecutionListener=\ diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java new file mode 100644 index 0000000000..c7fadb22ab --- /dev/null +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/AnnotationsPropertySourceTests.java @@ -0,0 +1,265 @@ +/* + * 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.properties; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link AnnotationsPropertySource}. + * + * @author Phillip Webb + */ +public class AnnotationsPropertySourceTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void createWhenSourceIsNullShouldThrowException() throws Exception { + this.thrown.expect(IllegalArgumentException.class); + this.thrown.expectMessage("Property source must not be null"); + new AnnotationsPropertySource(null); + } + + @Test + public void propertiesWhenHasNoAnnotationShouldBeEmpty() throws Exception { + AnnotationsPropertySource source = new AnnotationsPropertySource( + NoAnnotation.class); + assertThat(source.getPropertyNames()).isEmpty(); + assertThat(source.getProperty("value")).isNull(); + } + + @Test + public void propertiesWhenHasTypeLevelAnnotationShouldUseAttributeName() + throws Exception { + AnnotationsPropertySource source = new AnnotationsPropertySource(TypeLevel.class); + assertThat(source.getPropertyNames()).containsExactly("value"); + assertThat(source.getProperty("value")).isEqualTo("abc"); + } + + @Test + public void propertiesWhenHasTypeLevelWithPrefixShouldUsePrefixedName() + throws Exception { + AnnotationsPropertySource source = new AnnotationsPropertySource( + TypeLevelWithPrefix.class); + assertThat(source.getPropertyNames()).containsExactly("test.value"); + assertThat(source.getProperty("test.value")).isEqualTo("abc"); + } + + @Test + public void propertiesWhenHasAttributeLevelWithPrefixShouldUsePrefixedName() + throws Exception { + AnnotationsPropertySource source = new AnnotationsPropertySource( + AttributeLevelWithPrefix.class); + assertThat(source.getPropertyNames()).containsExactly("test"); + assertThat(source.getProperty("test")).isEqualTo("abc"); + } + + @Test + public void propertiesWhenHasTypeAndAttributeLevelWithPrefixShouldUsePrefixedName() + throws Exception { + AnnotationsPropertySource source = new AnnotationsPropertySource( + TypeAndAttributeLevelWithPrefix.class); + assertThat(source.getPropertyNames()).containsExactly("test.example"); + assertThat(source.getProperty("test.example")).isEqualTo("abc"); + } + + @Test + public void propertiesWhenNotMappedAtTypeLevelShouldIgnoreAttributes() + throws Exception { + AnnotationsPropertySource source = new AnnotationsPropertySource( + NotMappedAtTypeLevel.class); + assertThat(source.getPropertyNames()).containsExactly("value"); + assertThat(source.getProperty("ignore")).isNull(); + } + + @Test + public void propertiesWhenNotMappedAtAttributeLevelShouldIgnoreAttributes() + throws Exception { + AnnotationsPropertySource source = new AnnotationsPropertySource( + NotMappedAtAttributeLevel.class); + assertThat(source.getPropertyNames()).containsExactly("value"); + assertThat(source.getProperty("ignore")).isNull(); + } + + @Test + public void propertiesWhenCountainsArryasShouldExpandNames() throws Exception { + AnnotationsPropertySource source = new AnnotationsPropertySource(Arrays.class); + assertThat(source.getPropertyNames()).contains("strings[0]", "strings[1]", + "classes[0]", "classes[1]", "ints[0]", "ints[1]", "longs[0]", "longs[1]", + "floats[0]", "floats[1]", "doubles[0]", "doubles[1]", "booleans[0]", + "booleans[1]"); + assertThat(source.getProperty("strings[0]")).isEqualTo("a"); + assertThat(source.getProperty("strings[1]")).isEqualTo("b"); + assertThat(source.getProperty("classes[0]")).isEqualTo(Integer.class); + assertThat(source.getProperty("classes[1]")).isEqualTo(Long.class); + assertThat(source.getProperty("ints[0]")).isEqualTo(1); + assertThat(source.getProperty("ints[1]")).isEqualTo(2); + assertThat(source.getProperty("longs[0]")).isEqualTo(1L); + assertThat(source.getProperty("longs[1]")).isEqualTo(2L); + assertThat(source.getProperty("floats[0]")).isEqualTo(1.0f); + assertThat(source.getProperty("floats[1]")).isEqualTo(2.0f); + assertThat(source.getProperty("doubles[0]")).isEqualTo(1.0); + assertThat(source.getProperty("doubles[1]")).isEqualTo(2.0); + assertThat(source.getProperty("booleans[0]")).isEqualTo(false); + assertThat(source.getProperty("booleans[1]")).isEqualTo(true); + } + + @Test + public void propertiesWhenHasCamelCaseShouldConvertToKebabCase() throws Exception { + AnnotationsPropertySource source = new AnnotationsPropertySource( + CamelCaseToKebabCase.class); + assertThat(source.getPropertyNames()).contains("camel-case-to-kebab-case"); + } + + static class NoAnnotation { + + } + + @TypeLevelAnnotation("abc") + static class TypeLevel { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping + static @interface TypeLevelAnnotation { + + String value(); + + } + + @TypeLevelWithPrefixAnnotation("abc") + static class TypeLevelWithPrefix { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping("test") + static @interface TypeLevelWithPrefixAnnotation { + + String value(); + + } + + @AttributeLevelWithPrefixAnnotation("abc") + static class AttributeLevelWithPrefix { + + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AttributeLevelWithPrefixAnnotation { + + @PropertyMapping("test") + String value(); + + } + + @TypeAndAttributeLevelWithPrefixAnnotation("abc") + static class TypeAndAttributeLevelWithPrefix { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping("test") + static @interface TypeAndAttributeLevelWithPrefixAnnotation { + + @PropertyMapping("example") + String value(); + + } + + @NotMappedAtTypeLevelAnnotation("abc") + static class NotMappedAtTypeLevel { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping(map = false) + static @interface NotMappedAtTypeLevelAnnotation { + + @PropertyMapping + String value(); + + String ignore() default "xyz"; + + } + + @NotMappedAtAttributeLevelAnnotation("abc") + static class NotMappedAtAttributeLevel { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping + static @interface NotMappedAtAttributeLevelAnnotation { + + String value(); + + @PropertyMapping(map = false) + String ignore() default "xyz"; + + } + + @ArraysAnnotation(strings = { "a", "b" }, classes = { Integer.class, + Long.class }, ints = { 1, 2 }, longs = { 1, 2 }, floats = { 1.0f, + 2.0f }, doubles = { 1.0, 2.0 }, booleans = { false, true }) + static class Arrays { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping + static @interface ArraysAnnotation { + + String[] strings(); + + Class[] classes(); + + int[] ints(); + + long[] longs(); + + float[] floats(); + + double[] doubles(); + + boolean[] booleans(); + + } + + @CamelCaseToKebabCaseAnnotation + static class CamelCaseToKebabCase { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping + static @interface CamelCaseToKebabCaseAnnotation { + + String camelCaseToKebabCase() default "abc"; + + } + +} diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/ExampleMapping.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/ExampleMapping.java new file mode 100644 index 0000000000..782d89a06d --- /dev/null +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/ExampleMapping.java @@ -0,0 +1,33 @@ +/* + * 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.properties; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Example {@link PropertyMapping} annotation for use wuth {@link PropertyMappingTests}. + * + * @author Phillip Webb + */ +@Retention(RetentionPolicy.RUNTIME) +@PropertyMapping +@interface ExampleMapping { + + String exampleProperty(); + +} diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactoryTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactoryTests.java new file mode 100644 index 0000000000..a25f53df03 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingContextCustomizerFactoryTests.java @@ -0,0 +1,158 @@ +/* + * 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.properties; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.test.context.ContextCustomizer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; + +/** + * Tests for {@link PropertyMappingContextCustomizerFactory}. + * + * @author Phillip Webb + */ +public class PropertyMappingContextCustomizerFactoryTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private PropertyMappingContextCustomizerFactory factory = new PropertyMappingContextCustomizerFactory(); + + @Test + public void getContextCustomizerWhenHasNoMappingShouldNotAddPropertySource() { + ContextCustomizer customizer = this.factory + .createContextCustomizer(NoMapping.class, null); + ConfigurableApplicationContext context = mock( + ConfigurableApplicationContext.class); + ConfigurableEnvironment environment = mock(ConfigurableEnvironment.class); + ConfigurableListableBeanFactory beanFactory = mock( + ConfigurableListableBeanFactory.class); + given(context.getEnvironment()).willReturn(environment); + given(context.getBeanFactory()).willReturn(beanFactory); + customizer.customizeContext(context, null); + verifyZeroInteractions(environment); + } + + @Test + public void getContextCustomizerWhenHasTypeMappingShouldReturnCustomizer() { + ContextCustomizer customizer = this.factory + .createContextCustomizer(TypeMapping.class, null); + assertThat(customizer).isNotNull(); + } + + @Test + public void getContextCustomizerWhenHasAttributeMappingShouldReturnCustomizer() { + ContextCustomizer customizer = this.factory + .createContextCustomizer(AttributeMapping.class, null); + assertThat(customizer).isNotNull(); + } + + @Test + public void hashCodeAndEqualsShoudBeBasedOnPropertyValues() throws Exception { + ContextCustomizer customizer1 = this.factory + .createContextCustomizer(TypeMapping.class, null); + ContextCustomizer customizer2 = this.factory + .createContextCustomizer(AttributeMapping.class, null); + ContextCustomizer customizer3 = this.factory + .createContextCustomizer(OtherMapping.class, null); + assertThat(customizer1.hashCode()).isEqualTo(customizer2.hashCode()); + assertThat(customizer1).isEqualTo(customizer1).isEqualTo(customizer2) + .isNotEqualTo(customizer3); + } + + @Test + public void prepareContextShouldAddPropertySource() throws Exception { + ContextCustomizer customizer = this.factory + .createContextCustomizer(AttributeMapping.class, null); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + customizer.customizeContext(context, null); + assertThat(context.getEnvironment().getProperty("mapped")).isEqualTo("Mapped"); + } + + @Test + public void propertyMappingShouldNotBeUsedWithComponent() throws Exception { + ContextCustomizer customizer = this.factory + .createContextCustomizer(AttributeMapping.class, null); + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ConfigMapping.class); + customizer.customizeContext(context, null); + this.thrown.expect(BeanCreationException.class); + this.thrown.expectMessage( + "@PropertyMapping annotations can only be used on test classes"); + context.refresh(); + } + + @NoMappingAnnotation + static class NoMapping { + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface NoMappingAnnotation { + + } + + @TypeMappingAnnotation + static class TypeMapping { + } + + @Configuration + @TypeMappingAnnotation + static class ConfigMapping { + + } + + @Retention(RetentionPolicy.RUNTIME) + @PropertyMapping + static @interface TypeMappingAnnotation { + + String mapped() default "Mapped"; + + } + + @AttributeMappingAnnotation + static class AttributeMapping { + } + + @AttributeMappingAnnotation("Other") + static class OtherMapping { + } + + @Retention(RetentionPolicy.RUNTIME) + static @interface AttributeMappingAnnotation { + + @PropertyMapping("mapped") + String value() default "Mapped"; + + } + +} diff --git a/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingTests.java b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingTests.java new file mode 100644 index 0000000000..34df32ea40 --- /dev/null +++ b/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/properties/PropertyMappingTests.java @@ -0,0 +1,45 @@ +/* + * 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.properties; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.test.context.junit4.SpringRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link PropertyMapping @PropertyMapping} annotaions. + * + * @author Phillip Webb + */ +@RunWith(SpringRunner.class) +@ExampleMapping(exampleProperty = "abc") +public class PropertyMappingTests { + + @Autowired + private Environment environment; + + @Test + public void hasProperty() throws Exception { + assertThat(this.environment.getProperty("example-property")).isEqualTo("abc"); + } + +}