Skip to content

Commit

Permalink
Generate auto-configuration OnBean data
Browse files Browse the repository at this point in the history
Update the auto-configuration annotation processor to generate
properties for `@ConditionalOnBean` and `@ConditionalOnSingleCandidate`.

See gh-13328
  • Loading branch information
philwebb committed Sep 24, 2018
1 parent e4f54a4 commit 586507c
Show file tree
Hide file tree
Showing 8 changed files with 303 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@

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;
Expand All @@ -36,10 +37,8 @@
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;

Expand All @@ -52,6 +51,8 @@
*/
@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" })
Expand All @@ -60,21 +61,30 @@ public class AutoConfigureAnnotationProcessor extends AbstractProcessor {
protected static final String PROPERTIES_PATH = "META-INF/"
+ "spring-autoconfigure-metadata.properties";

private Map<String, String> annotations;
private final Map<String, String> annotations;

private final Map<String, ValueExtractor> valueExtractors;

private final Properties properties = new Properties();

public AutoConfigureAnnotationProcessor() {
Map<String, String> annotations = new LinkedHashMap<>();
addAnnotations(annotations);
this.annotations = Collections.unmodifiableMap(annotations);
Map<String, ValueExtractor> valueExtractors = new LinkedHashMap<>();
addValueExtractors(valueExtractors);
this.valueExtractors = Collections.unmodifiableMap(valueExtractors);
}

protected void addAnnotations(Map<String, String> annotations) {
annotations.put("Configuration",
"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",
Expand All @@ -83,6 +93,17 @@ protected void addAnnotations(Map<String, String> annotations) {
"org.springframework.boot.autoconfigure.AutoConfigureOrder");
}

private void addValueExtractors(Map<String, ValueExtractor> 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();
Expand Down Expand Up @@ -123,10 +144,10 @@ private void process(RoundEnvironment roundEnv, String propertyKey,
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<Object> values = getValues(annotation);
List<Object> values = getValues(propertyKey, annotation);
this.properties.put(qualifiedName + "." + propertyKey,
toCommaDelimitedString(values));
this.properties.put(qualifiedName, "");
Expand Down Expand Up @@ -158,68 +179,89 @@ private String toCommaDelimitedString(List<Object> list) {
return result.toString();
}

private List<Object> getValues(AnnotationMirror annotation) {
return annotation.getElementValues().entrySet().stream()
.filter(this::isNameOrValueAttribute).flatMap(this::getValues)
.collect(Collectors.toList());
}

private boolean isNameOrValueAttribute(Entry<? extends ExecutableElement, ?> entry) {
String attributeName = entry.getKey().getSimpleName().toString();
return "name".equals(attributeName) || "value".equals(attributeName);
private List<Object> 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<Object> getValues(Entry<?, ? extends AnnotationValue> entry) {
Object value = entry.getValue().getValue();
if (value instanceof List) {
return ((List<AnnotationValue>) 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<Object> getValues(AnnotationMirror annotation);

static ValueExtractor allFrom(String... attributes) {
Set<String> names = new HashSet<>(Arrays.asList(attributes));
return new AbstractValueExtractor() {

@Override
public List<Object> getValues(AnnotationMirror annotation) {
List<Object> 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<Object> 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<AnnotationValue>) 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<Object> getValues(AnnotationMirror annotation) {
Map<String, AnnotationValue> attributes = new LinkedHashMap<>();
annotation.getElementValues().forEach((key, value) -> attributes
.put(key.getSimpleName().toString(), value));
if (attributes.containsKey("name")) {
return Collections.emptyList();
}
List<Object> result = new ArrayList<>();
extractValues(attributes.get("value")).forEach(result::add);
extractValues(attributes.get("type")).forEach(result::add);
return result;
}

}

}
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -50,18 +50,38 @@ public void createCompiler() throws IOException {
@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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,6 +23,8 @@
*/
@TestConfiguration
@TestConditionalOnClass(name = "java.io.InputStream", value = TestClassConfiguration.Nested.class)
@TestConditionalOnBean(type = "java.io.OutputStream")
@TestConditionalOnSingleCandidate(type = "java.io.OutputStream")
public class TestClassConfiguration {

@TestAutoConfigureOrder
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -32,6 +32,8 @@
@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" })
Expand All @@ -48,6 +50,9 @@ public TestConditionMetadataAnnotationProcessor(File outputLocation) {
protected void addAnnotations(Map<String, String> 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);
Expand Down
Loading

0 comments on commit 586507c

Please sign in to comment.