diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessor.java index 6c6a893725..e6bad9588a 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessor.java @@ -18,14 +18,15 @@ package org.springframework.boot.autoconfigureprocessor; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Properties; import java.util.Set; -import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.processing.AbstractProcessor; @@ -36,10 +37,8 @@ import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeMirror; import javax.tools.FileObject; import javax.tools.StandardLocation; @@ -52,6 +51,8 @@ import javax.tools.StandardLocation; */ @SupportedAnnotationTypes({ "org.springframework.context.annotation.Configuration", "org.springframework.boot.autoconfigure.condition.ConditionalOnClass", + "org.springframework.boot.autoconfigure.condition.ConditionalOnBean", + "org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate", "org.springframework.boot.autoconfigure.AutoConfigureBefore", "org.springframework.boot.autoconfigure.AutoConfigureAfter", "org.springframework.boot.autoconfigure.AutoConfigureOrder" }) @@ -60,7 +61,9 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor { protected static final String PROPERTIES_PATH = "META-INF/" + "spring-autoconfigure-metadata.properties"; - private Map annotations; + private final Map annotations; + + private final Map valueExtractors; private final Properties properties = new Properties(); @@ -68,6 +71,9 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor { Map annotations = new LinkedHashMap<>(); addAnnotations(annotations); this.annotations = Collections.unmodifiableMap(annotations); + Map valueExtractors = new LinkedHashMap<>(); + addValueExtractors(valueExtractors); + this.valueExtractors = Collections.unmodifiableMap(valueExtractors); } protected void addAnnotations(Map annotations) { @@ -75,6 +81,10 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor { "org.springframework.context.annotation.Configuration"); annotations.put("ConditionalOnClass", "org.springframework.boot.autoconfigure.condition.ConditionalOnClass"); + annotations.put("ConditionalOnBean", + "org.springframework.boot.autoconfigure.condition.ConditionalOnBean"); + annotations.put("ConditionalOnSingleCandidate", + "org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate"); annotations.put("AutoConfigureBefore", "org.springframework.boot.autoconfigure.AutoConfigureBefore"); annotations.put("AutoConfigureAfter", @@ -83,6 +93,17 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor { "org.springframework.boot.autoconfigure.AutoConfigureOrder"); } + private void addValueExtractors(Map attributes) { + attributes.put("Configuration", ValueExtractor.allFrom("value")); + attributes.put("ConditionalOnClass", ValueExtractor.allFrom("value", "name")); + attributes.put("ConditionalOnBean", new OnBeanConditionValueExtractor()); + attributes.put("ConditionalOnSingleCandidate", + new OnBeanConditionValueExtractor()); + attributes.put("AutoConfigureBefore", ValueExtractor.allFrom("value", "name")); + attributes.put("AutoConfigureAfter", ValueExtractor.allFrom("value", "name")); + attributes.put("AutoConfigureOrder", ValueExtractor.allFrom("value")); + } + @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); @@ -123,10 +144,10 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor { private void processElement(Element element, String propertyKey, String annotationName) { try { - String qualifiedName = getQualifiedName(element); + String qualifiedName = Elements.getQualifiedName(element); AnnotationMirror annotation = getAnnotation(element, annotationName); if (qualifiedName != null && annotation != null) { - List values = getValues(annotation); + List values = getValues(propertyKey, annotation); this.properties.put(qualifiedName + "." + propertyKey, toCommaDelimitedString(values)); this.properties.put(qualifiedName, ""); @@ -158,68 +179,89 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor { return result.toString(); } - private List getValues(AnnotationMirror annotation) { - return annotation.getElementValues().entrySet().stream() - .filter(this::isNameOrValueAttribute).flatMap(this::getValues) - .collect(Collectors.toList()); - } - - private boolean isNameOrValueAttribute(Entry entry) { - String attributeName = entry.getKey().getSimpleName().toString(); - return "name".equals(attributeName) || "value".equals(attributeName); + private List getValues(String propertyKey, AnnotationMirror annotation) { + ValueExtractor extractor = this.valueExtractors.get(propertyKey); + if (extractor == null) { + return Collections.emptyList(); + } + return extractor.getValues(annotation); } - @SuppressWarnings("unchecked") - private Stream getValues(Entry entry) { - Object value = entry.getValue().getValue(); - if (value instanceof List) { - return ((List) value).stream() - .map((annotation) -> processValue(annotation.getValue())); + private void writeProperties() throws IOException { + if (!this.properties.isEmpty()) { + FileObject file = this.processingEnv.getFiler() + .createResource(StandardLocation.CLASS_OUTPUT, "", PROPERTIES_PATH); + try (OutputStream outputStream = file.openOutputStream()) { + this.properties.store(outputStream, null); + } } - return Stream.of(processValue(value)); } - private Object processValue(Object value) { - if (value instanceof DeclaredType) { - return getQualifiedName(((DeclaredType) value).asElement()); + @FunctionalInterface + private interface ValueExtractor { + + List getValues(AnnotationMirror annotation); + + static ValueExtractor allFrom(String... attributes) { + Set names = new HashSet<>(Arrays.asList(attributes)); + return new AbstractValueExtractor() { + + @Override + public List getValues(AnnotationMirror annotation) { + List result = new ArrayList<>(); + annotation.getElementValues().forEach((key, value) -> { + if (names.contains(key.getSimpleName().toString())) { + extractValues(value).forEach(result::add); + } + }); + return result; + } + + }; } - return value; + } - private String getQualifiedName(Element element) { - if (element != null) { - TypeElement enclosingElement = getEnclosingTypeElement(element.asType()); - if (enclosingElement != null) { - return getQualifiedName(enclosingElement) + "$" - + ((DeclaredType) element.asType()).asElement().getSimpleName() - .toString(); + private abstract static class AbstractValueExtractor implements ValueExtractor { + + @SuppressWarnings("unchecked") + protected Stream extractValues(AnnotationValue annotationValue) { + if (annotationValue == null) { + return Stream.empty(); } - if (element instanceof TypeElement) { - return ((TypeElement) element).getQualifiedName().toString(); + Object value = annotationValue.getValue(); + if (value instanceof List) { + return ((List) value).stream() + .map((annotation) -> extractValue(annotation.getValue())); } + return Stream.of(extractValue(value)); } - return null; - } - private TypeElement getEnclosingTypeElement(TypeMirror type) { - if (type instanceof DeclaredType) { - DeclaredType declaredType = (DeclaredType) type; - Element enclosingElement = declaredType.asElement().getEnclosingElement(); - if (enclosingElement != null && enclosingElement instanceof TypeElement) { - return (TypeElement) enclosingElement; + private Object extractValue(Object value) { + if (value instanceof DeclaredType) { + return Elements.getQualifiedName(((DeclaredType) value).asElement()); } + return value; } - return null; + } - private void writeProperties() throws IOException { - if (!this.properties.isEmpty()) { - FileObject file = this.processingEnv.getFiler() - .createResource(StandardLocation.CLASS_OUTPUT, "", PROPERTIES_PATH); - try (OutputStream outputStream = file.openOutputStream()) { - this.properties.store(outputStream, null); + private static class OnBeanConditionValueExtractor extends AbstractValueExtractor { + + @Override + public List getValues(AnnotationMirror annotation) { + Map attributes = new LinkedHashMap<>(); + annotation.getElementValues().forEach((key, value) -> attributes + .put(key.getSimpleName().toString(), value)); + if (attributes.containsKey("name")) { + return Collections.emptyList(); } + List result = new ArrayList<>(); + extractValues(attributes.get("value")).forEach(result::add); + extractValues(attributes.get("type")).forEach(result::add); + return result; } + } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/Elements.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/Elements.java new file mode 100644 index 0000000000..914c9beeea --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/main/java/org/springframework/boot/autoconfigureprocessor/Elements.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigureprocessor; + +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +/** + * Utilities for dealing with {@link Element} classes. + * + * @author Phillip Webb + */ +final class Elements { + + private Elements() { + } + + static String getQualifiedName(Element element) { + if (element != null) { + TypeElement enclosingElement = getEnclosingTypeElement(element.asType()); + if (enclosingElement != null) { + return getQualifiedName(enclosingElement) + "$" + + ((DeclaredType) element.asType()).asElement().getSimpleName() + .toString(); + } + if (element instanceof TypeElement) { + return ((TypeElement) element).getQualifiedName().toString(); + } + } + return null; + } + + private static TypeElement getEnclosingTypeElement(TypeMirror type) { + if (type instanceof DeclaredType) { + DeclaredType declaredType = (DeclaredType) type; + Element enclosingElement = declaredType.asElement().getEnclosingElement(); + if (enclosingElement != null && enclosingElement instanceof TypeElement) { + return (TypeElement) enclosingElement; + } + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessorTests.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessorTests.java index 929960d087..716bd8ab2f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessorTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/AutoConfigureAnnotationProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,18 +50,38 @@ public class AutoConfigureAnnotationProcessorTests { @Test public void annotatedClass() throws Exception { Properties properties = compile(TestClassConfiguration.class); - assertThat(properties).hasSize(3); + assertThat(properties).hasSize(5); assertThat(properties).containsEntry( "org.springframework.boot.autoconfigureprocessor." + "TestClassConfiguration.ConditionalOnClass", "java.io.InputStream,org.springframework.boot.autoconfigureprocessor." + "TestClassConfiguration$Nested"); - assertThat(properties).containsKey( - "org.springframework.boot.autoconfigureprocessor.TestClassConfiguration"); - assertThat(properties).containsKey( - "org.springframework.boot.autoconfigureprocessor.TestClassConfiguration.Configuration"); - assertThat(properties).doesNotContainKey( - "org.springframework.boot.autoconfigureprocessor.TestClassConfiguration$Nested"); + assertThat(properties) + .containsKey("org.springframework.boot.autoconfigureprocessor." + + "TestClassConfiguration"); + assertThat(properties) + .containsKey("org.springframework.boot.autoconfigureprocessor." + + "TestClassConfiguration.Configuration"); + assertThat(properties) + .doesNotContainKey("org.springframework.boot.autoconfigureprocessor." + + "TestClassConfiguration$Nested"); + assertThat(properties).containsEntry( + "org.springframework.boot.autoconfigureprocessor." + + "TestClassConfiguration.ConditionalOnBean", + "java.io.OutputStream"); + assertThat(properties).containsEntry( + "org.springframework.boot.autoconfigureprocessor." + + "TestClassConfiguration.ConditionalOnSingleCandidate", + "java.io.OutputStream"); + } + + @Test + public void annoatedClassWithOnBeanThatHasName() throws Exception { + Properties properties = compile(TestOnBeanWithNameClassConfiguration.class); + assertThat(properties).hasSize(3); + assertThat(properties).containsEntry( + "org.springframework.boot.autoconfigureprocessor.TestOnBeanWithNameClassConfiguration.ConditionalOnBean", + ""); } @Test diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestClassConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestClassConfiguration.java index 3bc346b1ec..6cd7433609 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestClassConfiguration.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestClassConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,8 @@ package org.springframework.boot.autoconfigureprocessor; */ @TestConfiguration @TestConditionalOnClass(name = "java.io.InputStream", value = TestClassConfiguration.Nested.class) +@TestConditionalOnBean(type = "java.io.OutputStream") +@TestConditionalOnSingleCandidate(type = "java.io.OutputStream") public class TestClassConfiguration { @TestAutoConfigureOrder diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestConditionMetadataAnnotationProcessor.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestConditionMetadataAnnotationProcessor.java index 5647100796..178d8e793f 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestConditionMetadataAnnotationProcessor.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestConditionMetadataAnnotationProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * Copyright 2012-2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,8 @@ import javax.annotation.processing.SupportedAnnotationTypes; @SupportedAnnotationTypes({ "org.springframework.boot.autoconfigureprocessor.TestConfiguration", "org.springframework.boot.autoconfigureprocessor.TestConditionalOnClass", + "org.springframework.boot.autoconfigure.condition.TestConditionalOnBean", + "org.springframework.boot.autoconfigure.condition.TestConditionalOnSingleCandidate", "org.springframework.boot.autoconfigureprocessor.TestAutoConfigureBefore", "org.springframework.boot.autoconfigureprocessor.TestAutoConfigureAfter", "org.springframework.boot.autoconfigureprocessor.TestAutoConfigureOrder" }) @@ -48,6 +50,9 @@ public class TestConditionMetadataAnnotationProcessor protected void addAnnotations(Map annotations) { put(annotations, "Configuration", TestConfiguration.class); put(annotations, "ConditionalOnClass", TestConditionalOnClass.class); + put(annotations, "ConditionalOnBean", TestConditionalOnBean.class); + put(annotations, "ConditionalOnSingleCandidate", + TestConditionalOnSingleCandidate.class); put(annotations, "AutoConfigureBefore", TestAutoConfigureBefore.class); put(annotations, "AutoConfigureAfter", TestAutoConfigureAfter.class); put(annotations, "AutoConfigureOrder", TestAutoConfigureOrder.class); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestConditionalOnBean.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestConditionalOnBean.java new file mode 100644 index 0000000000..7c22b99abd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestConditionalOnBean.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigureprocessor; + +import java.lang.annotation.Annotation; +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; + +/** + * Alternative to Spring Boot's {@code ConditionalOnBean} for testing (removes the need + * for a dependency on the real annotation). + * + * @author Phillip Webb + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TestConditionalOnBean { + + Class[] value() default {}; + + String[] type() default {}; + + Class[] annotation() default {}; + + String[] name() default {}; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestConditionalOnSingleCandidate.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestConditionalOnSingleCandidate.java new file mode 100644 index 0000000000..51e103e725 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestConditionalOnSingleCandidate.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigureprocessor; + +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; + +/** + * Alternative to Spring Boot's {@code ConditionalOnSingleCandidate} for testing (removes + * the need for a dependency on the real annotation). + * + * @author Phillip Webb + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface TestConditionalOnSingleCandidate { + + Class value() default Object.class; + + String type() default ""; + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestOnBeanWithNameClassConfiguration.java b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestOnBeanWithNameClassConfiguration.java new file mode 100644 index 0000000000..3f4f87e9bd --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-autoconfigure-processor/src/test/java/org/springframework/boot/autoconfigureprocessor/TestOnBeanWithNameClassConfiguration.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigureprocessor; + +/** + * Test configuration with an annotated class. + * + * @author Phillip Webb + */ +@TestConfiguration +@TestConditionalOnBean(name = "test", type = "java.io.OutputStream") +public class TestOnBeanWithNameClassConfiguration { + +}