From 884c058e573ff3bf3824aea414f56485d514d00d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 31 Oct 2014 10:18:28 -0700 Subject: [PATCH 1/5] Add processor to generate configuration meta-data Adds an annotation processor to generates a JSON meta-data file at compile time from @ConfigurationProperties items. Each meta-data file can include an array or 'properties' and 'groups'. A 'property' is a single item that may appear in a Spring Boot 'application.properties' file with a given value. For example, 'server.port' and 'server.context-path' are properties. Each property may optionally include 'type' and 'description' attributes to provide the data type (e.g. `java.lang.Integer`, `java.lang.String`) and some short documentation (taken from the field javadoc) about what the property is for. For consistency, the type of a primitive is translated to its wrapper counterpart, i.e. `boolean` becomes `java.lang.Boolean`. A 'group' provides a higher level grouping of properties. For example the 'server.port' and 'server.context-path' properties are in the 'server' group. Both 'property' and 'group' items may additional have 'sourceType' and 'sourceMethod' attributes to indicate the source that contributed them. Users may use `META-INF/additional-spring-configuration-metadata.json` to manually provide additionally meta-data that is not covered by @ConfigurationProperties objects. The contents of this file will be read and merged with harvested items. The complete meta-data file is finally written to `META-INF/spring-configuration-metadata.json`. See gh-1001 --- spring-boot-dependencies/pom.xml | 5 + spring-boot-parent/pom.xml | 3 +- spring-boot-tools/pom.xml | 1 + .../pom.xml | 40 +++ ...figurationMetadataAnnotationProcessor.java | 253 ++++++++++++++++ .../TypeElementMembers.java | 123 ++++++++ .../configurationprocessor/TypeUtils.java | 120 ++++++++ .../metadata/ConfigurationMetadata.java | 87 ++++++ .../metadata/ItemMetadata.java | 128 ++++++++ .../metadata/JsonMarshaller.java | 140 +++++++++ .../javax.annotation.processing.Processor | 1 + ...ationMetadataAnnotationProcessorTests.java | 286 ++++++++++++++++++ .../ConfigurationMetadataMatchers.java | 161 ++++++++++ .../configurationprocessor/TestCompiler.java | 96 ++++++ .../metadata/JsonMarshallerTests.java | 61 ++++ .../ConfigurationProperties.java | 41 +++ .../method/EmptyTypeMethodConfig.java | 48 +++ .../method/InvalidMethodConfig.java | 44 +++ .../method/MethodAndClassConfig.java | 67 ++++ .../method/SimpleMethodConfig.java | 56 ++++ .../simple/HierarchicalProperties.java | 39 +++ .../HierarchicalPropertiesGrandparent.java | 36 +++ .../simple/HierarchicalPropertiesParent.java | 49 +++ .../simple/NotAnnotated.java | 27 ++ .../simple/SimpleCollectionProperties.java | 84 +++++ .../simple/SimplePrefixValueProperties.java | 39 +++ .../simple/SimpleProperties.java | 93 ++++++ .../simple/SimpleTypeProperties.java | 199 ++++++++++++ .../InnerClassAnnotatedGetterConfig.java | 59 ++++ .../specific/InnerClassProperties.java | 89 ++++++ .../specific/InnerClassRootConfig.java | 42 +++ .../specific/SimplePojo.java | 36 +++ 32 files changed, 2552 insertions(+), 1 deletion(-) create mode 100644 spring-boot-tools/spring-boot-configuration-processor/pom.xml create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestCompiler.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ConfigurationProperties.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/EmptyTypeMethodConfig.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/InvalidMethodConfig.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/MethodAndClassConfig.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/SimpleMethodConfig.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalProperties.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesGrandparent.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesParent.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/NotAnnotated.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleCollectionProperties.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimplePrefixValueProperties.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleProperties.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleTypeProperties.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassAnnotatedGetterConfig.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassRootConfig.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/SimplePojo.java diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 111bcc6c83..1ad9576456 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -159,6 +159,11 @@ spring-boot-autoconfigure 1.2.0.BUILD-SNAPSHOT + + org.springframework.boot + spring-boot-configuration-processor + 1.2.0.BUILD-SNAPSHOT + org.springframework.boot spring-boot-dependency-tools diff --git a/spring-boot-parent/pom.xml b/spring-boot-parent/pom.xml index 0275a7c49e..cf48644065 100644 --- a/spring-boot-parent/pom.xml +++ b/spring-boot-parent/pom.xml @@ -19,9 +19,10 @@ .. 1.6 + 0.9.1.v20140329 + 20140107 UTF-8 UTF-8 - 0.9.1.v20140329 20140107 3.1.1 diff --git a/spring-boot-tools/pom.xml b/spring-boot-tools/pom.xml index a21fbe528e..6dfefa64f2 100644 --- a/spring-boot-tools/pom.xml +++ b/spring-boot-tools/pom.xml @@ -20,6 +20,7 @@ ${basedir}/.. + spring-boot-configuration-processor spring-boot-dependency-tools spring-boot-loader spring-boot-loader-tools diff --git a/spring-boot-tools/spring-boot-configuration-processor/pom.xml b/spring-boot-tools/spring-boot-configuration-processor/pom.xml new file mode 100644 index 0000000000..71970bbe48 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-tools + 1.2.0.BUILD-SNAPSHOT + + spring-boot-configuration-processor + Spring Boot Configuration Processor + Spring Boot Configuration Processor + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + + org.json + json + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + none + + + + + diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java new file mode 100644 index 0000000000..eaee1f5e5e --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -0,0 +1,253 @@ +/* + * Copyright 2012-2014 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.configurationprocessor; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; +import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; +import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller; + +/** + * Annotation {@link Processor} that writes meta-data file for + * {@code @ConfigurationProperties}. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.2.0 + */ +@SupportedAnnotationTypes({ ConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_ANNOTATION }) +@SupportedSourceVersion(SourceVersion.RELEASE_6) +public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor { + + static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot." + + "context.properties.ConfigurationProperties"; + + private ConfigurationMetadata metadata; + + private TypeUtils typeUtils; + + protected String configurationPropertiesAnnotation() { + return CONFIGURATION_PROPERTIES_ANNOTATION; + } + + @Override + public synchronized void init(ProcessingEnvironment env) { + super.init(env); + this.metadata = new ConfigurationMetadata(); + this.typeUtils = new TypeUtils(env); + } + + @Override + public boolean process(Set annotations, + RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + processElement(element); + } + } + if (roundEnv.processingOver()) { + writeMetaData(this.metadata); + } + return false; + } + + private void processElement(Element element) { + AnnotationMirror annotation = getAnnotation(element, + configurationPropertiesAnnotation()); + String prefix = getPrefix(annotation); + if (annotation != null) { + if (element instanceof TypeElement) { + processAnnotatedTypeElement(prefix, (TypeElement) element); + } + else if (element instanceof ExecutableElement) { + processExecutableElement(prefix, (ExecutableElement) element); + } + } + } + + private void processAnnotatedTypeElement(String prefix, TypeElement element) { + String type = this.typeUtils.getType(element); + this.metadata.add(ItemMetadata.newGroup(prefix, type, type, null)); + processTypeElement(prefix, element); + } + + private void processExecutableElement(String prefix, ExecutableElement element) { + if (element.getModifiers().contains(Modifier.PUBLIC) + && (TypeKind.VOID != element.getReturnType().getKind())) { + Element returns = this.processingEnv.getTypeUtils().asElement( + element.getReturnType()); + if (returns instanceof TypeElement) { + this.metadata.add(ItemMetadata.newGroup(prefix, + this.typeUtils.getType(returns), + this.typeUtils.getType(element.getEnclosingElement()), + element.toString())); + processTypeElement(prefix, (TypeElement) returns); + } + } + } + + private void processTypeElement(String prefix, TypeElement element) { + TypeElementMembers members = new TypeElementMembers(this.processingEnv, element); + processSimpleTypes(prefix, element, members); + processNestedTypes(prefix, element, members); + } + + private void processSimpleTypes(String prefix, TypeElement element, + TypeElementMembers members) { + for (Map.Entry entry : members.getPublicGetters() + .entrySet()) { + String name = entry.getKey(); + ExecutableElement getter = entry.getValue(); + ExecutableElement setter = members.getPublicSetters().get(name); + VariableElement field = members.getFields().get(name); + if (setter != null + || this.typeUtils.isCollectionOrMap(getter.getReturnType())) { + String dataType = this.typeUtils.getType(getter.getReturnType()); + String sourceType = this.typeUtils.getType(element); + String description = this.typeUtils.getJavaDoc(field); + this.metadata.add(ItemMetadata.newProperty(prefix, name, dataType, + sourceType, null, description)); + } + } + } + + private void processNestedTypes(String prefix, TypeElement element, + TypeElementMembers members) { + for (Map.Entry entry : members.getPublicGetters() + .entrySet()) { + ExecutableElement getter = entry.getValue(); + Element returnType = this.processingEnv.getTypeUtils().asElement( + getter.getReturnType()); + AnnotationMirror annotation = getAnnotation(getter, + configurationPropertiesAnnotation()); + if (returnType != null && returnType instanceof TypeElement + && annotation == null) { + TypeElement returns = (TypeElement) returnType; + if (this.typeUtils.isEnclosedIn(returnType, element)) { + String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, + entry.getKey()); + this.metadata.add(ItemMetadata.newGroup(nestedPrefix, + this.typeUtils.getType(returns), + this.typeUtils.getType(element), getter.toString())); + processTypeElement(nestedPrefix, returns); + } + } + } + } + + private AnnotationMirror getAnnotation(Element element, String type) { + for (AnnotationMirror annotation : element.getAnnotationMirrors()) { + if (type.equals(annotation.getAnnotationType().toString())) { + return annotation; + } + } + return null; + } + + private String getPrefix(AnnotationMirror annotation) { + Map elementValues = getAnnotationElementValues(annotation); + Object prefix = elementValues.get("prefix"); + if (prefix != null && !"".equals(prefix)) { + return (String) prefix; + } + Object value = elementValues.get("value"); + if (value != null && !"".equals(value)) { + return (String) value; + } + return null; + } + + private Map getAnnotationElementValues(AnnotationMirror annotation) { + Map values = new LinkedHashMap(); + for (Map.Entry entry : annotation + .getElementValues().entrySet()) { + values.put(entry.getKey().getSimpleName().toString(), entry.getValue() + .getValue()); + } + return values; + } + + protected void writeMetaData(ConfigurationMetadata metadata) { + metadata = mergeManualMetadata(metadata); + try { + FileObject resource = this.processingEnv.getFiler().createResource( + StandardLocation.CLASS_OUTPUT, "", + "META-INF/spring-configuration-metadata.json"); + OutputStream outputStream = resource.openOutputStream(); + try { + new JsonMarshaller().write(metadata, outputStream); + } + finally { + outputStream.close(); + } + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + + private ConfigurationMetadata mergeManualMetadata(ConfigurationMetadata metadata) { + try { + FileObject manualMetadata = this.processingEnv.getFiler().getResource( + StandardLocation.CLASS_PATH, "", + "META-INF/additional-spring-configuration-metadata.json"); + InputStream inputStream = manualMetadata.openInputStream(); + try { + ConfigurationMetadata merged = new ConfigurationMetadata(metadata); + try { + merged.addAll(new JsonMarshaller().read(inputStream)); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + return merged; + } + finally { + inputStream.close(); + } + } + catch (IOException ex) { + ex.printStackTrace(); + return metadata; + } + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java new file mode 100644 index 0000000000..4a5832c3bd --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java @@ -0,0 +1,123 @@ +/* + * Copyright 2012-2014 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.configurationprocessor; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.util.ElementFilter; + +/** + * Provides access to relevant {@link TypeElement} members. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.2.0 + */ +class TypeElementMembers { + + private static final String OBJECT_CLASS_NAME = Object.class.getName(); + + private final ProcessingEnvironment env; + + private final Map fields = new LinkedHashMap(); + + private final Map publicGetters = new LinkedHashMap(); + + private final Map publicSetters = new LinkedHashMap(); + + public TypeElementMembers(ProcessingEnvironment env, TypeElement element) { + this.env = env; + process(element); + } + + private void process(TypeElement element) { + for (ExecutableElement method : ElementFilter.methodsIn(element + .getEnclosedElements())) { + processMethod(method); + } + for (VariableElement field : ElementFilter + .fieldsIn(element.getEnclosedElements())) { + processField(field); + } + Element superType = this.env.getTypeUtils().asElement(element.getSuperclass()); + if (superType != null && superType instanceof TypeElement + && !OBJECT_CLASS_NAME.equals(superType.toString())) { + process((TypeElement) superType); + } + } + + private void processMethod(ExecutableElement method) { + if (method.getModifiers().contains(Modifier.PUBLIC)) { + String name = method.getSimpleName().toString(); + if (isGetter(method) && !this.publicGetters.containsKey(name)) { + this.publicGetters.put(getAccessorName(name), method); + } + else if (isSetter(method) && !this.publicSetters.containsKey(name)) { + this.publicSetters.put(getAccessorName(name), method); + } + } + } + + private boolean isGetter(ExecutableElement method) { + String name = method.getSimpleName().toString(); + return (name.startsWith("get") || name.startsWith("is")) + && method.getParameters().isEmpty() + && (TypeKind.VOID != method.getReturnType().getKind()); + } + + private boolean isSetter(ExecutableElement method) { + final String name = method.getSimpleName().toString(); + return name.startsWith("set") && method.getParameters().size() == 1 + && (TypeKind.VOID == method.getReturnType().getKind()); + } + + private String getAccessorName(String methodName) { + String name = methodName.startsWith("is") ? methodName.substring(2) : methodName + .substring(3); + name = Character.toLowerCase(name.charAt(0)) + name.substring(1); + return name; + } + + private void processField(VariableElement field) { + String name = field.getSimpleName().toString(); + if (!this.fields.containsKey(name)) { + this.fields.put(name, field); + } + } + + public Map getFields() { + return Collections.unmodifiableMap(this.fields); + } + + public Map getPublicGetters() { + return Collections.unmodifiableMap(this.publicGetters); + } + + public Map getPublicSetters() { + return Collections.unmodifiableMap(this.publicSetters); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java new file mode 100644 index 0000000000..10789f8665 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2014 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.configurationprocessor; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.WildcardType; +import javax.lang.model.util.Types; + +/** + * Type Utilities. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.2.0 + */ +class TypeUtils { + + private static final Map> PRIMITIVE_WRAPPERS; + static { + Map> wrappers = new HashMap>(); + wrappers.put(TypeKind.BOOLEAN, Boolean.class); + wrappers.put(TypeKind.BYTE, Byte.class); + wrappers.put(TypeKind.CHAR, Character.class); + wrappers.put(TypeKind.DOUBLE, Double.class); + wrappers.put(TypeKind.FLOAT, Float.class); + wrappers.put(TypeKind.INT, Integer.class); + wrappers.put(TypeKind.LONG, Long.class); + wrappers.put(TypeKind.SHORT, Short.class); + PRIMITIVE_WRAPPERS = Collections.unmodifiableMap(wrappers); + } + + private final ProcessingEnvironment env; + + private final TypeMirror collectionType; + + private final TypeMirror mapType; + + public TypeUtils(ProcessingEnvironment env) { + this.env = env; + Types types = env.getTypeUtils(); + WildcardType wc = types.getWildcardType(null, null); + this.collectionType = types.getDeclaredType(this.env.getElementUtils() + .getTypeElement(Collection.class.getName()), wc); + this.mapType = types.getDeclaredType( + this.env.getElementUtils().getTypeElement(Map.class.getName()), wc, wc); + + } + + public String getType(Element element) { + return getType(element == null ? null : element.asType()); + } + + public String getType(TypeMirror type) { + if (type == null) { + return null; + } + Class wrapper = PRIMITIVE_WRAPPERS.get(type.getKind()); + if (wrapper != null) { + return wrapper.getName(); + } + if (type instanceof DeclaredType) { + DeclaredType declaredType = (DeclaredType) type; + Element enclosingElement = declaredType.asElement().getEnclosingElement(); + if (enclosingElement != null && enclosingElement instanceof TypeElement) { + return getType(enclosingElement) + "$" + + declaredType.asElement().getSimpleName().toString(); + } + } + return type.toString(); + } + + public boolean isCollectionOrMap(TypeMirror type) { + return this.env.getTypeUtils().isAssignable(type, this.collectionType) + || this.env.getTypeUtils().isAssignable(type, this.mapType); + } + + public boolean isEnclosedIn(Element candidate, TypeElement element) { + if (candidate == null || element == null) { + return false; + } + if (candidate.equals(element)) { + return true; + } + return isEnclosedIn(candidate.getEnclosingElement(), element); + } + + public String getJavaDoc(Element element) { + String javadoc = (element == null ? null : this.env.getElementUtils() + .getDocComment(element)); + if (javadoc != null) { + javadoc = javadoc.trim(); + } + return ("".equals(javadoc) ? null : javadoc); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java new file mode 100644 index 0000000000..8d44ffe180 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.metadata; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Configuration meta-data. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.2.0 + * @see ItemMetadata + */ +public class ConfigurationMetadata { + + private final List items; + + public ConfigurationMetadata() { + this.items = new ArrayList(); + } + + public ConfigurationMetadata(ConfigurationMetadata metadata) { + this.items = new ArrayList(metadata.getItems()); + } + + /** + * Add item meta-data. + * @param itemMetadata the meta-data to add + */ + public void add(ItemMetadata itemMetadata) { + this.items.add(itemMetadata); + Collections.sort(this.items); + } + + /** + * Add all properties from another {@link ConfigurationMetadata}. + * @param metadata the {@link ConfigurationMetadata} instance to merge + */ + public void addAll(ConfigurationMetadata metadata) { + this.items.addAll(metadata.getItems()); + Collections.sort(this.items); + } + + /** + * @return the meta-data properties. + */ + public List getItems() { + return Collections.unmodifiableList(this.items); + } + + public static String nestedPrefix(String prefix, String name) { + String nestedPrefix = (prefix == null ? "" : prefix); + String dashedName = toDashedCase(name); + nestedPrefix += ("".equals(nestedPrefix) ? dashedName : "." + dashedName); + return nestedPrefix; + } + + static String toDashedCase(String name) { + StringBuilder dashed = new StringBuilder(); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (Character.isUpperCase(c) && dashed.length() > 0) { + dashed.append("-"); + } + dashed.append(Character.toLowerCase(c)); + } + return dashed.toString(); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java new file mode 100644 index 0000000000..e0b4fd309f --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.metadata; + +/** + * A group or property meta-data item from some {@link ConfigurationMetadata}. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.2.0 + * @see ConfigurationMetadata + */ +public class ItemMetadata implements Comparable { + + private final ItemType itemType; + + private final String name; + + private final String type; + + private final String description; + + private final String sourceType; + + private String sourceMethod; + + ItemMetadata(ItemType itemType, String prefix, String name, String type, + String sourceType, String sourceMethod, String description) { + super(); + this.itemType = itemType; + this.name = buildName(prefix, name); + this.type = type; + this.sourceType = sourceType; + this.sourceMethod = sourceMethod; + this.description = description; + } + + private String buildName(String prefix, String name) { + while (prefix != null && prefix.endsWith(".")) { + prefix = prefix.substring(0, prefix.length() - 1); + } + StringBuilder fullName = new StringBuilder(prefix == null ? "" : prefix); + if (fullName.length() > 0 && name != null) { + fullName.append("."); + } + fullName.append(name == null ? "" : ConfigurationMetadata.toDashedCase(name)); + return fullName.toString(); + } + + public boolean isOfItemType(ItemType itemType) { + return this.itemType == itemType; + } + + public String getName() { + return this.name; + } + + public String getType() { + return this.type; + } + + public String getSourceType() { + return this.sourceType; + } + + public String getSourceMethod() { + return this.sourceMethod; + } + + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + StringBuilder string = new StringBuilder(this.name); + buildToStringProperty(string, "type", this.type); + buildToStringProperty(string, "sourceType", this.sourceType); + buildToStringProperty(string, "description", this.description); + return string.toString(); + } + + protected final void buildToStringProperty(StringBuilder string, String property, + Object value) { + if (value != null) { + string.append(" ").append(property).append(":").append(value); + } + } + + @Override + public int compareTo(ItemMetadata o) { + return getName().compareTo(o.getName()); + } + + public static ItemMetadata newGroup(String name, String type, String sourceType, + String sourceMethod) { + return new ItemMetadata(ItemType.GROUP, name, null, type, sourceType, + sourceMethod, null); + } + + public static ItemMetadata newProperty(String prefix, String name, String type, + String sourceType, String sourceMethod, String description) { + return new ItemMetadata(ItemType.PROPERTY, prefix, name, type, sourceType, + sourceMethod, description); + } + + /** + * The item type. + */ + public static enum ItemType { + GROUP, PROPERTY + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java new file mode 100644 index 0000000000..7d6f345996 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java @@ -0,0 +1,140 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.metadata; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.boot.configurationprocessor.metadata.ItemMetadata.ItemType; + +/** + * Marshaller to write {@link ConfigurationMetadata} as JSON. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.2.0 + */ +public class JsonMarshaller { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private static final int BUFFER_SIZE = 4098; + + public void write(ConfigurationMetadata metadata, OutputStream outputStream) + throws IOException { + JSONObject object = new JSONObject(); + object.put("groups", toJsonArray(metadata, ItemType.GROUP)); + object.put("properties", toJsonArray(metadata, ItemType.PROPERTY)); + outputStream.write(object.toString(2).getBytes(UTF_8)); + } + + private JSONArray toJsonArray(ConfigurationMetadata metadata, ItemType itemType) { + JSONArray jsonArray = new JSONArray(); + for (ItemMetadata item : metadata.getItems()) { + if (item.isOfItemType(itemType)) { + jsonArray.put(toJsonObject(item)); + } + } + return jsonArray; + } + + private JSONObject toJsonObject(ItemMetadata item) { + JSONObject jsonObject = new JSONOrderedObject(); + jsonObject.put("name", item.getName()); + putIfPresent(jsonObject, "type", item.getType()); + putIfPresent(jsonObject, "description", item.getDescription()); + putIfPresent(jsonObject, "sourceType", item.getSourceType()); + putIfPresent(jsonObject, "sourceMethod", item.getSourceMethod()); + return jsonObject; + } + + private void putIfPresent(JSONObject jsonObject, String name, Object value) { + if (value != null) { + jsonObject.put(name, value); + } + } + + public ConfigurationMetadata read(InputStream inputStream) throws IOException { + ConfigurationMetadata metadata = new ConfigurationMetadata(); + JSONObject object = new JSONObject(toString(inputStream)); + JSONArray groups = object.optJSONArray("groups"); + if (groups != null) { + for (int i = 0; i < groups.length(); i++) { + metadata.add(toItemMetadata((JSONObject) groups.get(i), ItemType.GROUP)); + } + } + JSONArray properties = object.optJSONArray("properties"); + if (properties != null) { + for (int i = 0; i < properties.length(); i++) { + metadata.add(toItemMetadata((JSONObject) properties.get(i), + ItemType.PROPERTY)); + } + } + return metadata; + } + + private ItemMetadata toItemMetadata(JSONObject object, ItemType itemType) { + String name = object.getString("name"); + String type = object.optString("type", null); + String description = object.optString("description", null); + String sourceType = object.optString("sourceType", null); + String sourceMethod = object.optString("sourceMethod", null); + return new ItemMetadata(itemType, name, null, type, sourceType, sourceMethod, + description); + } + + private String toString(InputStream inputStream) throws IOException { + StringBuilder out = new StringBuilder(); + InputStreamReader reader = new InputStreamReader(inputStream, UTF_8); + char[] buffer = new char[BUFFER_SIZE]; + int bytesRead; + while ((bytesRead = reader.read(buffer)) != -1) { + out.append(buffer, 0, bytesRead); + } + return out.toString(); + } + + /** + * Extension to {@link JSONObject} that remembers the order of inserts. + */ + @SuppressWarnings("rawtypes") + private static class JSONOrderedObject extends JSONObject { + + private Set keys = new LinkedHashSet(); + + @Override + public JSONObject put(String key, Object value) throws JSONException { + this.keys.add(key); + return super.put(key, value); + } + + @Override + public Set keySet() { + return this.keys; + } + + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/spring-boot-tools/spring-boot-configuration-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000..e01209af7c --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +org.springframework.boot.configurationprocessor.ConfigurationMetadataAnnotationProcessor diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java new file mode 100644 index 0000000000..f90af42742 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -0,0 +1,286 @@ +/* + * Copyright 2012-2014 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.configurationprocessor; + +import java.io.IOException; + +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; +import org.springframework.boot.configurationsample.method.EmptyTypeMethodConfig; +import org.springframework.boot.configurationsample.method.InvalidMethodConfig; +import org.springframework.boot.configurationsample.method.MethodAndClassConfig; +import org.springframework.boot.configurationsample.method.SimpleMethodConfig; +import org.springframework.boot.configurationsample.simple.HierarchicalProperties; +import org.springframework.boot.configurationsample.simple.NotAnnotated; +import org.springframework.boot.configurationsample.simple.SimpleCollectionProperties; +import org.springframework.boot.configurationsample.simple.SimplePrefixValueProperties; +import org.springframework.boot.configurationsample.simple.SimpleProperties; +import org.springframework.boot.configurationsample.simple.SimpleTypeProperties; +import org.springframework.boot.configurationsample.specific.InnerClassAnnotatedGetterConfig; +import org.springframework.boot.configurationsample.specific.InnerClassProperties; +import org.springframework.boot.configurationsample.specific.InnerClassRootConfig; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsGroup; +import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsProperty; + +/** + * Tests for {@link ConfigurationMetadataAnnotationProcessor}. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +public class ConfigurationMetadataAnnotationProcessorTests { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void notAnnotated() throws Exception { + ConfigurationMetadata metadata = compile(NotAnnotated.class); + assertThat("No config metadata file should have been generated when " + + "no metadata is discovered", metadata, nullValue()); + } + + @Test + public void simpleProperties() throws Exception { + ConfigurationMetadata metadata = compile(SimpleProperties.class); + assertThat(metadata, containsGroup("simple").fromSource(SimpleProperties.class)); + assertThat( + metadata, + containsProperty("simple.the-name", String.class).fromSource( + SimpleProperties.class).withDescription( + "The name of this simple properties.")); + assertThat( + metadata, + containsProperty("simple.flag", Boolean.class).fromSource( + SimpleProperties.class).withDescription("A simple flag.")); + assertThat(metadata, containsProperty("simple.comparator")); + assertThat(metadata, not(containsProperty("simple.counter"))); + assertThat(metadata, not(containsProperty("simple.size"))); + } + + @Test + public void simplePrefixValueProperties() throws Exception { + ConfigurationMetadata metadata = compile(SimplePrefixValueProperties.class); + assertThat(metadata, + containsGroup("simple").fromSource(SimplePrefixValueProperties.class)); + assertThat( + metadata, + containsProperty("simple.name", String.class).fromSource( + SimplePrefixValueProperties.class)); + } + + @Test + public void simpleTypeProperties() throws Exception { + ConfigurationMetadata metadata = compile(SimpleTypeProperties.class); + assertThat(metadata, + containsGroup("simple.type").fromSource(SimpleTypeProperties.class)); + assertThat(metadata, containsProperty("simple.type.my-string", String.class)); + assertThat(metadata, containsProperty("simple.type.my-byte", Byte.class)); + assertThat(metadata, + containsProperty("simple.type.my-primitive-byte", Byte.class)); + assertThat(metadata, containsProperty("simple.type.my-char", Character.class)); + assertThat(metadata, + containsProperty("simple.type.my-primitive-char", Character.class)); + assertThat(metadata, containsProperty("simple.type.my-boolean", Boolean.class)); + assertThat(metadata, + containsProperty("simple.type.my-primitive-boolean", Boolean.class)); + assertThat(metadata, containsProperty("simple.type.my-short", Short.class)); + assertThat(metadata, + containsProperty("simple.type.my-primitive-short", Short.class)); + assertThat(metadata, containsProperty("simple.type.my-integer", Integer.class)); + assertThat(metadata, + containsProperty("simple.type.my-primitive-integer", Integer.class)); + assertThat(metadata, containsProperty("simple.type.my-long", Long.class)); + assertThat(metadata, + containsProperty("simple.type.my-primitive-long", Long.class)); + assertThat(metadata, containsProperty("simple.type.my-double", Double.class)); + assertThat(metadata, + containsProperty("simple.type.my-primitive-double", Double.class)); + assertThat(metadata, containsProperty("simple.type.my-float", Float.class)); + assertThat(metadata, + containsProperty("simple.type.my-primitive-float", Float.class)); + assertThat(metadata.getItems().size(), equalTo(18)); + } + + @Test + public void hierarchicalProperties() throws Exception { + ConfigurationMetadata metadata = compile(HierarchicalProperties.class); + assertThat(metadata, + containsGroup("hierarchical").fromSource(HierarchicalProperties.class)); + assertThat(metadata, containsProperty("hierarchical.first", String.class) + .fromSource(HierarchicalProperties.class)); + assertThat(metadata, containsProperty("hierarchical.second", String.class) + .fromSource(HierarchicalProperties.class)); + assertThat(metadata, containsProperty("hierarchical.third", String.class) + .fromSource(HierarchicalProperties.class)); + } + + @Test + public void parseCollectionConfig() throws Exception { + ConfigurationMetadata metadata = compile(SimpleCollectionProperties.class); + // getter and setter + assertThat( + metadata, + containsProperty("collection.integers-to-names", + "java.util.Map")); + assertThat( + metadata, + containsProperty("collection.longs", + "java.util.Collection")); + assertThat(metadata, + containsProperty("collection.floats", "java.util.List")); + // getter only + assertThat( + metadata, + containsProperty("collection.names-to-integers", + "java.util.Map")); + assertThat( + metadata, + containsProperty("collection.bytes", + "java.util.Collection")); + assertThat( + metadata, + containsProperty("collection.doubles", "java.util.List")); + } + + @Test + public void simpleMethodConfig() throws Exception { + ConfigurationMetadata metadata = compile(SimpleMethodConfig.class); + assertThat(metadata, containsGroup("foo").fromSource(SimpleMethodConfig.class)); + assertThat( + metadata, + containsProperty("foo.name", String.class).fromSource( + SimpleMethodConfig.Foo.class)); + assertThat( + metadata, + containsProperty("foo.flag", Boolean.class).fromSource( + SimpleMethodConfig.Foo.class)); + } + + @Test + public void invalidMethodConfig() throws Exception { + ConfigurationMetadata metadata = compile(InvalidMethodConfig.class); + assertThat( + metadata, + containsProperty("something.name", String.class).fromSource( + InvalidMethodConfig.class)); + assertThat(metadata, not(containsProperty("invalid.name"))); + } + + @Test + public void methodAndClassConfig() throws Exception { + ConfigurationMetadata metadata = compile(MethodAndClassConfig.class); + assertThat( + metadata, + containsProperty("conflict.name", String.class).fromSource( + MethodAndClassConfig.Foo.class)); + assertThat( + metadata, + containsProperty("conflict.flag", Boolean.class).fromSource( + MethodAndClassConfig.Foo.class)); + assertThat( + metadata, + containsProperty("conflict.value", String.class).fromSource( + MethodAndClassConfig.class)); + } + + @Test + public void emptyTypeMethodConfig() throws Exception { + ConfigurationMetadata metadata = compile(EmptyTypeMethodConfig.class); + assertThat(metadata, not(containsProperty("something.foo"))); + } + + @Test + public void innerClassRootConfig() throws Exception { + ConfigurationMetadata metadata = compile(InnerClassRootConfig.class); + assertThat(metadata, containsProperty("config.name")); + } + + @Test + public void innerClassProperties() throws Exception { + ConfigurationMetadata metadata = compile(InnerClassProperties.class); + assertThat(metadata, + containsGroup("config").fromSource(InnerClassProperties.class)); + assertThat(metadata, + containsGroup("config.first").ofType(InnerClassProperties.Foo.class) + .fromSource(InnerClassProperties.class)); + assertThat(metadata, containsProperty("config.first.name")); + assertThat(metadata, containsProperty("config.first.bar.name")); + assertThat(metadata, + containsProperty("config.the-second", InnerClassProperties.Foo.class) + .fromSource(InnerClassProperties.class)); + assertThat(metadata, containsProperty("config.the-second.name")); + assertThat(metadata, containsProperty("config.the-second.bar.name")); + assertThat( + metadata, + containsGroup("config.third").ofType( + InnerClassProperties.SimplePojo.class).fromSource( + InnerClassProperties.class)); + assertThat(metadata, containsProperty("config.third.value")); + } + + @Test + public void innerClassAnnotatedGetterConfig() throws Exception { + ConfigurationMetadata metadata = compile(InnerClassAnnotatedGetterConfig.class); + assertThat(metadata, containsProperty("specific.value")); + assertThat(metadata, containsProperty("foo.name")); + assertThat(metadata, not(containsProperty("specific.foo"))); + } + + private ConfigurationMetadata compile(Class... types) throws IOException { + TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor(); + new TestCompiler(this.temporaryFolder).getTask(types).call(processor); + return processor.getMetadata(); + } + + @SupportedAnnotationTypes({ TestConfigurationMetadataAnnotationProcessor.CONFIGURATION_PROPERTIES_ANNOTATION }) + @SupportedSourceVersion(SourceVersion.RELEASE_6) + private static class TestConfigurationMetadataAnnotationProcessor extends + ConfigurationMetadataAnnotationProcessor { + + static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.configurationsample.ConfigurationProperties"; + + private ConfigurationMetadata metadata; + + @Override + protected String configurationPropertiesAnnotation() { + return CONFIGURATION_PROPERTIES_ANNOTATION; + } + + @Override + protected void writeMetaData(ConfigurationMetadata metadata) { + this.metadata = metadata; + } + + public ConfigurationMetadata getMetadata() { + return this.metadata; + } + + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java new file mode 100644 index 0000000000..c796172c13 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2014 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.configurationprocessor; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; +import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; +import org.springframework.boot.configurationprocessor.metadata.ItemMetadata.ItemType; + +/** + * Hamcrest {@link Matcher} to help test {@link ConfigurationMetadata}. + * + * @author Phillip Webb + */ +public class ConfigurationMetadataMatchers { + + public static ContainsItemMatcher containsGroup(String name) { + return new ContainsItemMatcher(ItemType.GROUP, name); + } + + public static ContainsItemMatcher containsGroup(String name, Class type) { + return new ContainsItemMatcher(ItemType.GROUP, name).ofType(type); + } + + public static ContainsItemMatcher containsGroup(String name, String type) { + return new ContainsItemMatcher(ItemType.GROUP, name).ofDataType(type); + } + + public static ContainsItemMatcher containsProperty(String name) { + return new ContainsItemMatcher(ItemType.PROPERTY, name); + } + + public static ContainsItemMatcher containsProperty(String name, Class type) { + return new ContainsItemMatcher(ItemType.PROPERTY, name).ofType(type); + } + + public static ContainsItemMatcher containsProperty(String name, String type) { + return new ContainsItemMatcher(ItemType.PROPERTY, name).ofDataType(type); + } + + public static class ContainsItemMatcher extends BaseMatcher { + + private final ItemType itemType; + + private final String name; + + private final String type; + + private final Class sourceType; + + private final String description; + + public ContainsItemMatcher(ItemType itemType, String name) { + this(itemType, name, null, null, null); + } + + public ContainsItemMatcher(ItemType itemType, String name, String type, + Class sourceType, String description) { + this.itemType = itemType; + this.name = name; + this.type = type; + this.sourceType = sourceType; + this.description = description; + } + + @Override + public boolean matches(Object item) { + ConfigurationMetadata metadata = (ConfigurationMetadata) item; + ItemMetadata itemMetadata = getFirstPropertyWithName(metadata, this.name); + if (itemMetadata == null) { + return false; + } + if (this.type != null && !this.type.equals(itemMetadata.getType())) { + return false; + } + if (this.sourceType != null + && !this.sourceType.getName().equals(itemMetadata.getSourceType())) { + return false; + } + if (this.description != null + && !this.description.equals(itemMetadata.getDescription())) { + return false; + } + return true; + } + + @Override + public void describeMismatch(Object item, Description description) { + ConfigurationMetadata metadata = (ConfigurationMetadata) item; + ItemMetadata property = getFirstPropertyWithName(metadata, this.name); + if (property == null) { + description.appendText("missing property " + this.name); + } + else { + description.appendText("was property ").appendValue(property); + } + } + + @Override + public void describeTo(Description description) { + description.appendText("metadata containing " + this.name); + if (this.type != null) { + description.appendText(" dataType ").appendValue(this.type); + } + if (this.sourceType != null) { + description.appendText(" sourceType ").appendValue(this.sourceType); + } + if (this.description != null) { + description.appendText(" description ").appendValue(this.description); + } + } + + public ContainsItemMatcher ofType(Class dataType) { + return new ContainsItemMatcher(this.itemType, this.name, dataType.getName(), + this.sourceType, this.description); + } + + public ContainsItemMatcher ofDataType(String dataType) { + return new ContainsItemMatcher(this.itemType, this.name, dataType, + this.sourceType, this.description); + } + + public ContainsItemMatcher fromSource(Class sourceType) { + return new ContainsItemMatcher(this.itemType, this.name, this.type, + sourceType, this.description); + } + + public ContainsItemMatcher withDescription(String description) { + return new ContainsItemMatcher(this.itemType, this.name, this.type, + this.sourceType, description); + } + + private ItemMetadata getFirstPropertyWithName(ConfigurationMetadata metadata, + String name) { + for (ItemMetadata item : metadata.getItems()) { + if (item.isOfItemType(this.itemType) && name.equals(item.getName())) { + return item; + } + } + return null; + } + + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestCompiler.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestCompiler.java new file mode 100644 index 0000000000..b10d0ece5d --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestCompiler.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2014 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.configurationprocessor; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +import javax.annotation.processing.Processor; +import javax.tools.JavaCompiler; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; + +import org.junit.rules.TemporaryFolder; + +/** + * Wrapper to make the {@link JavaCompiler} easier to use in tests. + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +public class TestCompiler { + + private final JavaCompiler compiler; + + private final StandardJavaFileManager fileManager; + + public TestCompiler(TemporaryFolder temporaryFolder) throws IOException { + this(ToolProvider.getSystemJavaCompiler(), temporaryFolder); + } + + public TestCompiler(JavaCompiler compiler, TemporaryFolder temporaryFolder) + throws IOException { + this.compiler = compiler; + this.fileManager = compiler.getStandardFileManager(null, null, null); + Iterable temp = Arrays.asList(temporaryFolder.newFolder()); + this.fileManager.setLocation(StandardLocation.CLASS_OUTPUT, temp); + this.fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, temp); + } + + public TestCompilationTask getTask(Class... types) { + Iterable javaFileObjects = getJavaFileObjects(types); + return new TestCompilationTask(this.compiler.getTask(null, this.fileManager, + null, null, null, javaFileObjects)); + } + + private Iterable getJavaFileObjects(Class... types) { + File[] files = new File[types.length]; + for (int i = 0; i < types.length; i++) { + files[i] = getFile(types[i]); + } + return this.fileManager.getJavaFileObjects(files); + } + + private File getFile(Class type) { + return new File("src/test/java/" + type.getName().replace(".", "/") + ".java"); + } + + /** + * A compilation task. + */ + public static class TestCompilationTask { + + private final CompilationTask task; + + public TestCompilationTask(CompilationTask task) { + this.task = task; + } + + public void call(Processor... processors) { + this.task.setProcessors(Arrays.asList(processors)); + if (!this.task.call()) { + throw new IllegalStateException("Compilation failed"); + } + } + + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java new file mode 100644 index 0000000000..d7cf0efa2c --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.metadata; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Test; + +import static org.junit.Assert.assertThat; +import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsGroup; +import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsProperty; + +/** + * Tests for {@link JsonMarshaller}. + * + * @author Phillip Webb + */ +public class JsonMarshallerTests { + + @Test + public void marshallAndUnmarshal() throws IOException { + ConfigurationMetadata metadata = new ConfigurationMetadata(); + metadata.add(ItemMetadata.newProperty("a", "b", StringBuffer.class.getName(), + InputStream.class.getName(), "sourceMethod", "desc")); + metadata.add(ItemMetadata.newProperty("b.c.d", null, null, null, null, null)); + metadata.add(ItemMetadata.newProperty("c", null, null, null, null, null)); + metadata.add(ItemMetadata.newProperty("d", null, null, null, null, null)); + metadata.add(ItemMetadata.newGroup("d", null, null, null)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + JsonMarshaller marshaller = new JsonMarshaller(); + marshaller.write(metadata, outputStream); + System.out.println(outputStream); + ConfigurationMetadata read = marshaller.read(new ByteArrayInputStream( + outputStream.toByteArray())); + assertThat(read, + containsProperty("a.b", StringBuffer.class).fromSource(InputStream.class) + .withDescription("desc")); + assertThat(read, containsProperty("b.c.d")); + assertThat(read, containsProperty("c")); + assertThat(read, containsProperty("d")); + assertThat(read, containsGroup("d")); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ConfigurationProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ConfigurationProperties.java new file mode 100644 index 0000000000..1f53f555ea --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/ConfigurationProperties.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2014 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.configurationsample; + +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 @ConfigurationProperties} for testing (removes the + * need for a dependency on the real annotation). + * + * @author Stephane Nicoll + * @author Phillip Webb + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface ConfigurationProperties { + + String value() default ""; + + String prefix() default ""; + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/EmptyTypeMethodConfig.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/EmptyTypeMethodConfig.java new file mode 100644 index 0000000000..2797be051a --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/EmptyTypeMethodConfig.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2014 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.configurationsample.method; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Sample for testing method configuration. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("something") +public class EmptyTypeMethodConfig { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @ConfigurationProperties("something") + public Foo foo() { + return new Foo(); + } + + public static class Foo { + + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/InvalidMethodConfig.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/InvalidMethodConfig.java new file mode 100644 index 0000000000..adff4f45fe --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/InvalidMethodConfig.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2014 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.configurationsample.method; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Sample for testing invalid method configuration. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties(prefix = "something") +public class InvalidMethodConfig { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @ConfigurationProperties(prefix = "invalid") + InvalidMethodConfig foo() { + return new InvalidMethodConfig(); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/MethodAndClassConfig.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/MethodAndClassConfig.java new file mode 100644 index 0000000000..d59d120a73 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/MethodAndClassConfig.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2014 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.configurationsample.method; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Sample for testing mixed method and class configuration. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("conflict") +public class MethodAndClassConfig { + + private String value; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + @ConfigurationProperties(prefix = "conflict") + public Foo foo() { + return new Foo(); + } + + public static class Foo { + + private String name; + + private boolean flag; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isFlag() { + return this.flag; + } + + public void setFlag(boolean flag) { + this.flag = flag; + } + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/SimpleMethodConfig.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/SimpleMethodConfig.java new file mode 100644 index 0000000000..93b6755d37 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/method/SimpleMethodConfig.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2014 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.configurationsample.method; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Sample for testing simple method configuration. + * + * @author Stephane Nicoll + */ +public class SimpleMethodConfig { + + @ConfigurationProperties(prefix = "foo") + public Foo foo() { + return new Foo(); + } + + public static class Foo { + + private String name; + + private boolean flag; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isFlag() { + return this.flag; + } + + public void setFlag(boolean flag) { + this.flag = flag; + } + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalProperties.java new file mode 100644 index 0000000000..66211c885b --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalProperties.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2014 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.configurationsample.simple; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Configuration properties with inherited values. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties(prefix = "hierarchical") +public class HierarchicalProperties extends HierarchicalPropertiesParent { + + private String third; + + public String getThird() { + return this.third; + } + + public void setThird(String third) { + this.third = third; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesGrandparent.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesGrandparent.java new file mode 100644 index 0000000000..dc18e89109 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesGrandparent.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2014 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.configurationsample.simple; + +/** + * Grandparent for {@link HierarchicalProperties}. + * + * @author Stephane Nicoll + */ +public abstract class HierarchicalPropertiesGrandparent { + + private String first; + + public String getFirst() { + return this.first; + } + + public void setFirst(String first) { + this.first = first; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesParent.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesParent.java new file mode 100644 index 0000000000..0baf41aeb3 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/HierarchicalPropertiesParent.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2014 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.configurationsample.simple; + +/** + * Parent for {@link HierarchicalProperties}. + * + * @author Stephane Nicoll + */ +public abstract class HierarchicalPropertiesParent extends + HierarchicalPropertiesGrandparent { + + private String second; + + public String getSecond() { + return this.second; + } + + public void setSecond(String second) { + this.second = second; + } + + // Useless override + + @Override + public String getFirst() { + return super.getFirst(); + } + + @Override + public void setFirst(String first) { + super.setFirst(first); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/NotAnnotated.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/NotAnnotated.java new file mode 100644 index 0000000000..8346236a47 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/NotAnnotated.java @@ -0,0 +1,27 @@ +/* + * Copyright 2012-2014 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.configurationsample.simple; + + +/** + * This has no annotation on purpose to check that no meta-data is generated. + * + * @author Stephane Nicoll + */ +public class NotAnnotated { + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleCollectionProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleCollectionProperties.java new file mode 100644 index 0000000000..91d14019a2 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleCollectionProperties.java @@ -0,0 +1,84 @@ +/* + * Copyright 2012-2014 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.configurationsample.simple; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Properties with collections. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties(prefix = "collection") +public class SimpleCollectionProperties { + + private Map integersToNames; + + private Collection longs; + + private List floats; + + private final Map namesToIntegers = new HashMap(); + + private final Collection bytes = new LinkedHashSet(); + + private final List doubles = new ArrayList(); + + public Map getIntegersToNames() { + return this.integersToNames; + } + + public void setIntegersToNames(Map integersToNames) { + this.integersToNames = integersToNames; + } + + public Collection getLongs() { + return this.longs; + } + + public void setLongs(Collection longs) { + this.longs = longs; + } + + public List getFloats() { + return this.floats; + } + + public void setFloats(List floats) { + this.floats = floats; + } + + public Map getNamesToIntegers() { + return this.namesToIntegers; + } + + public Collection getBytes() { + return this.bytes; + } + + public List getDoubles() { + return this.doubles; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimplePrefixValueProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimplePrefixValueProperties.java new file mode 100644 index 0000000000..284fb4afdd --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimplePrefixValueProperties.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2014 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.configurationsample.simple; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Properties with a simple prefix. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("simple") +public class SimplePrefixValueProperties { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleProperties.java new file mode 100644 index 0000000000..c5e4379f36 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleProperties.java @@ -0,0 +1,93 @@ +/* + * Copyright 2012-2014 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.configurationsample.simple; + +import java.beans.FeatureDescriptor; +import java.util.Comparator; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Simple properties. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties(prefix = "simple") +public class SimpleProperties { + + /** + * The name of this simple properties. + */ + private String theName = "boot"; + + // isFlag is also detected + /** + * A simple flag. + */ + private boolean flag; + + // An interface can still be injected because it might have a converter + private Comparator comparator; + + // There is only a getter on this instance but we don't know what to do with it -> + // ignored + private FeatureDescriptor featureDescriptor; + + // There is only a setter on this "simple" property --> ignored + @SuppressWarnings("unused") + private Long counter; + + // There is only a getter on this "simple" property --> ignored + private Integer size; + + public String getTheName() { + return this.theName; + } + + public void setTheName(String name) { + this.theName = name; + } + + public boolean isFlag() { + return this.flag; + } + + public void setFlag(boolean flag) { + this.flag = flag; + } + + public Comparator getComparator() { + return this.comparator; + } + + public void setComparator(Comparator comparator) { + this.comparator = comparator; + } + + public FeatureDescriptor getFeatureDescriptor() { + return this.featureDescriptor; + } + + public void setCounter(Long counter) { + this.counter = counter; + } + + public Integer getSize() { + return this.size; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleTypeProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleTypeProperties.java new file mode 100644 index 0000000000..a6f7778df4 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/simple/SimpleTypeProperties.java @@ -0,0 +1,199 @@ +/* + * Copyright 2012-2014 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.configurationsample.simple; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Expose simple types to make sure these are detected properly. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties(prefix = "simple.type") +public class SimpleTypeProperties { + + private String myString; + + private Byte myByte; + + private byte myPrimitiveByte; + + private Character myChar; + + private char myPrimitiveChar; + + private Boolean myBoolean; + + private boolean myPrimitiveBoolean; + + private Short myShort; + + private short myPrimitiveShort; + + private Integer myInteger; + + private int myPrimitiveInteger; + + private Long myLong; + + private long myPrimitiveLong; + + private Double myDouble; + + private double myPrimitiveDouble; + + private Float myFloat; + + private float myPrimitiveFloat; + + public String getMyString() { + return this.myString; + } + + public void setMyString(String myString) { + this.myString = myString; + } + + public Byte getMyByte() { + return this.myByte; + } + + public void setMyByte(Byte myByte) { + this.myByte = myByte; + } + + public byte getMyPrimitiveByte() { + return this.myPrimitiveByte; + } + + public void setMyPrimitiveByte(byte myPrimitiveByte) { + this.myPrimitiveByte = myPrimitiveByte; + } + + public Character getMyChar() { + return this.myChar; + } + + public void setMyChar(Character myChar) { + this.myChar = myChar; + } + + public char getMyPrimitiveChar() { + return this.myPrimitiveChar; + } + + public void setMyPrimitiveChar(char myPrimitiveChar) { + this.myPrimitiveChar = myPrimitiveChar; + } + + public Boolean getMyBoolean() { + return this.myBoolean; + } + + public void setMyBoolean(Boolean myBoolean) { + this.myBoolean = myBoolean; + } + + public boolean isMyPrimitiveBoolean() { + return this.myPrimitiveBoolean; + } + + public void setMyPrimitiveBoolean(boolean myPrimitiveBoolean) { + this.myPrimitiveBoolean = myPrimitiveBoolean; + } + + public Short getMyShort() { + return this.myShort; + } + + public void setMyShort(Short myShort) { + this.myShort = myShort; + } + + public short getMyPrimitiveShort() { + return this.myPrimitiveShort; + } + + public void setMyPrimitiveShort(short myPrimitiveShort) { + this.myPrimitiveShort = myPrimitiveShort; + } + + public Integer getMyInteger() { + return this.myInteger; + } + + public void setMyInteger(Integer myInteger) { + this.myInteger = myInteger; + } + + public int getMyPrimitiveInteger() { + return this.myPrimitiveInteger; + } + + public void setMyPrimitiveInteger(int myPrimitiveInteger) { + this.myPrimitiveInteger = myPrimitiveInteger; + } + + public Long getMyLong() { + return this.myLong; + } + + public void setMyLong(Long myLong) { + this.myLong = myLong; + } + + public long getMyPrimitiveLong() { + return this.myPrimitiveLong; + } + + public void setMyPrimitiveLong(long myPrimitiveLong) { + this.myPrimitiveLong = myPrimitiveLong; + } + + public Double getMyDouble() { + return this.myDouble; + } + + public void setMyDouble(Double myDouble) { + this.myDouble = myDouble; + } + + public double getMyPrimitiveDouble() { + return this.myPrimitiveDouble; + } + + public void setMyPrimitiveDouble(double myPrimitiveDouble) { + this.myPrimitiveDouble = myPrimitiveDouble; + } + + public Float getMyFloat() { + return this.myFloat; + } + + public void setMyFloat(Float myFloat) { + this.myFloat = myFloat; + } + + public float getMyPrimitiveFloat() { + return this.myPrimitiveFloat; + } + + public void setMyPrimitiveFloat(float myPrimitiveFloat) { + this.myPrimitiveFloat = myPrimitiveFloat; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassAnnotatedGetterConfig.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassAnnotatedGetterConfig.java new file mode 100644 index 0000000000..18cb060146 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassAnnotatedGetterConfig.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2014 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.configurationsample.specific; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Demonstrate that a method that exposes a root group within an annotated class is + * ignored as it should. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties("specific") +public class InnerClassAnnotatedGetterConfig { + + private String value; + + public String getValue() { + return this.value; + } + + public void setValue(String value) { + this.value = value; + } + + @ConfigurationProperties("foo") + public Foo getFoo() { + return new Foo(); + } + + public static class Foo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java new file mode 100644 index 0000000000..d9791a7571 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2014 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.configurationsample.specific; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Demonstrate the auto-detection of a inner config classes. + * + * @author Stephane Nicoll + */ +@ConfigurationProperties(prefix = "config") +public class InnerClassProperties { + + private final Foo first = new Foo(); + + private Foo second = new Foo(); + + private final SimplePojo third = new SimplePojo(); + + public Foo getFirst() { + return this.first; + } + + public Foo getTheSecond() { + return this.second; + } + + public void setTheSecond(Foo second) { + this.second = second; + } + + public SimplePojo getThird() { + return this.third; + } + + public static class Foo { + + private String name; + + private final Bar bar = new Bar(); + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Bar getBar() { + return this.bar; + } + + public static class Bar { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + } + + } + + public static class SimplePojo extends + org.springframework.boot.configurationsample.specific.SimplePojo { + + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassRootConfig.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassRootConfig.java new file mode 100644 index 0000000000..6910b57f55 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassRootConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2014 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.configurationsample.specific; + +import org.springframework.boot.configurationsample.ConfigurationProperties; + +/** + * Sample with a simple inner class config. + * + * @author Stephane Nicoll + */ +public class InnerClassRootConfig { + + @ConfigurationProperties(prefix = "config") + static class Config { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/SimplePojo.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/SimplePojo.java new file mode 100644 index 0000000000..14fc86cd47 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/SimplePojo.java @@ -0,0 +1,36 @@ +/* + * Copyright 2012-2014 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.configurationsample.specific; + +/** + * POJO for use with samples. + * + * @author Stephane Nicoll + */ +public class SimplePojo { + + private int value; + + public int getValue() { + return this.value; + } + + public void setValue(int value) { + this.value = value; + } + +} From c73adcd198fe2088049484bc4441aa47392884c3 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 28 Oct 2014 15:09:09 -0700 Subject: [PATCH 2/5] Add a defaultValue attribute to config meta-data Update `ConfigurationMetadataAnnotationProcessor` to include the 'defaultValue' of a property when possible. For example the 'defaultValue' or 'server.port' is '8080'. Default values are detected by inspecting the field assignments of @ConfigurationProperties items. In order to detect field values some internals of the Java compiler are used. To save a dependency on 'tools.jar' internal javac classes are accessed using reflection. See gh-1001 --- ...figurationMetadataAnnotationProcessor.java | 35 ++++- .../fieldvalues/FieldValuesParser.java | 56 ++++++++ .../fieldvalues/javac/ExpressionTree.java | 48 +++++++ .../javac/JavaCompilerFieldValuesParser.java | 130 ++++++++++++++++++ .../fieldvalues/javac/ReflectionWrapper.java | 80 +++++++++++ .../fieldvalues/javac/Tree.java | 83 +++++++++++ .../fieldvalues/javac/TreeVisitor.java | 29 ++++ .../fieldvalues/javac/Trees.java | 48 +++++++ .../fieldvalues/javac/VariableTree.java | 59 ++++++++ .../metadata/ItemMetadata.java | 18 ++- .../metadata/JsonMarshaller.java | 4 +- ...ationMetadataAnnotationProcessorTests.java | 10 +- .../ConfigurationMetadataMatchers.java | 27 +++- .../AbstractFieldValuesProcessorTests.java | 124 +++++++++++++++++ ...JavaCompilerFieldValuesProcessorTests.java | 45 ++++++ .../metadata/JsonMarshallerTests.java | 15 +- .../fieldvalues/FieldValues.class | Bin 0 -> 1603 bytes .../fieldvalues/FieldValues.java | 78 +++++++++++ 18 files changed, 864 insertions(+), 25 deletions(-) create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/FieldValuesParser.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Tree.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/TreeVisitor.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/VariableTree.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesProcessorTests.java create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/FieldValues.class create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/FieldValues.java diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index eaee1f5e5e..f2a08639d4 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -19,6 +19,7 @@ package org.springframework.boot.configurationprocessor; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; @@ -38,9 +39,12 @@ import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; +import javax.tools.Diagnostic.Kind; import javax.tools.FileObject; import javax.tools.StandardLocation; +import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser; +import org.springframework.boot.configurationprocessor.fieldvalues.javac.JavaCompilerFieldValuesParser; import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller; @@ -64,6 +68,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor private TypeUtils typeUtils; + private FieldValuesParser fieldValuesParser; + protected String configurationPropertiesAnnotation() { return CONFIGURATION_PROPERTIES_ANNOTATION; } @@ -73,6 +79,18 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor super.init(env); this.metadata = new ConfigurationMetadata(); this.typeUtils = new TypeUtils(env); + try { + this.fieldValuesParser = new JavaCompilerFieldValuesParser(env); + } + catch (Throwable ex) { + this.fieldValuesParser = FieldValuesParser.NONE; + logWarning("Field value processing of @ConfigurationProperty meta-data is " + + "not supported"); + } + } + + private void logWarning(String msg) { + this.processingEnv.getMessager().printMessage(Kind.WARNING, msg); } @Override @@ -126,12 +144,22 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor private void processTypeElement(String prefix, TypeElement element) { TypeElementMembers members = new TypeElementMembers(this.processingEnv, element); - processSimpleTypes(prefix, element, members); + Map fieldValues = getFieldValues(element); + processSimpleTypes(prefix, element, members, fieldValues); processNestedTypes(prefix, element, members); } + private Map getFieldValues(TypeElement element) { + try { + return this.fieldValuesParser.getFieldValues(element); + } + catch (Exception ex) { + return Collections.emptyMap(); + } + } + private void processSimpleTypes(String prefix, TypeElement element, - TypeElementMembers members) { + TypeElementMembers members, Map fieldValues) { for (Map.Entry entry : members.getPublicGetters() .entrySet()) { String name = entry.getKey(); @@ -143,8 +171,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor String dataType = this.typeUtils.getType(getter.getReturnType()); String sourceType = this.typeUtils.getType(element); String description = this.typeUtils.getJavaDoc(field); + Object defaultValue = fieldValues.get(name); this.metadata.add(ItemMetadata.newProperty(prefix, name, dataType, - sourceType, null, description)); + sourceType, null, description, defaultValue)); } } } diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/FieldValuesParser.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/FieldValuesParser.java new file mode 100644 index 0000000000..e2e73d3a7f --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/FieldValuesParser.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.fieldvalues; + +import java.util.Collections; +import java.util.Map; + +import javax.lang.model.element.TypeElement; + +import org.springframework.boot.configurationprocessor.fieldvalues.javac.JavaCompilerFieldValuesParser; + +/** + * Parser which can be used to obtain the field values from an {@link TypeElement}. + * + * @author Phillip Webb + * @since 1.1.2 + * @see JavaCompilerFieldValuesParser + */ +public interface FieldValuesParser { + + /** + * Implementation of {@link FieldValuesParser} that always returns an empty + * result. + */ + public static final FieldValuesParser NONE = new FieldValuesParser() { + + @Override + public Map getFieldValues(TypeElement element) { + return Collections.emptyMap(); + } + + }; + + /** + * Return the field values for the given element. + * @param element the element to inspect + * @return a map of field names to values. + * @throws Exception if the values cannot be extracted + */ + Map getFieldValues(TypeElement element) throws Exception; + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java new file mode 100644 index 0000000000..6ad9cc77f2 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.fieldvalues.javac; + +import java.lang.reflect.Method; + +/** + * Reflection based access to {@code com.sun.source.tree.ExpressionTree}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +class ExpressionTree extends ReflectionWrapper { + + private final Class literalTreeType = findClass("com.sun.source.tree.LiteralTree"); + + private final Method literalValueMethod = findMethod(this.literalTreeType, "getValue"); + + public ExpressionTree(Object instance) { + super(instance); + } + + public String getKind() throws Exception { + return findMethod("getKind").invoke(getInstance()).toString(); + } + + public Object getLiteralValue() throws Exception { + if (this.literalTreeType.isAssignableFrom(getInstance().getClass())) { + return this.literalValueMethod.invoke(getInstance()); + } + return null; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java new file mode 100644 index 0000000000..8f7e28b1df --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java @@ -0,0 +1,130 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.fieldvalues.javac; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; + +import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser; + +/** + * {@link FieldValuesParser} implementation for the standard Java compiler. + * + * @author Phillip Webb + * @since 1.2.0 + */ +public class JavaCompilerFieldValuesParser implements FieldValuesParser { + + private final Trees trees; + + public JavaCompilerFieldValuesParser(ProcessingEnvironment env) throws Exception { + this.trees = Trees.instance(env); + } + + @Override + public Map getFieldValues(TypeElement element) throws Exception { + Tree tree = this.trees.getTree(element); + if (tree != null) { + FieldCollector fieldCollector = new FieldCollector(); + tree.accept(fieldCollector); + return fieldCollector.getFieldValues(); + } + return Collections.emptyMap(); + } + + private static class FieldCollector implements TreeVisitor { + + private static final Map> WRAPPER_TYPES; + static { + Map> types = new HashMap>(); + types.put("boolean", Boolean.class); + types.put(Boolean.class.getName(), Boolean.class); + types.put("byte", Byte.class); + types.put(Byte.class.getName(), Byte.class); + types.put("short", Short.class); + types.put(Short.class.getName(), Short.class); + types.put("int", Integer.class); + types.put(Integer.class.getName(), Integer.class); + types.put("long", Long.class); + types.put(Long.class.getName(), Long.class); + WRAPPER_TYPES = Collections.unmodifiableMap(types); + } + + private static final Map, Object> DEFAULT_TYPE_VALUES; + static { + Map, Object> values = new HashMap, Object>(); + values.put(Boolean.class, false); + values.put(Byte.class, (byte) 0); + values.put(Short.class, (short) 0); + values.put(Integer.class, 0); + values.put(Long.class, (long) 0); + DEFAULT_TYPE_VALUES = Collections.unmodifiableMap(values); + } + + private static final Map WELL_KNOWN_STATIC_FINALS; + static { + Map values = new HashMap(); + values.put("Boolean.TRUE", true); + values.put("Boolean.FALSE", false); + WELL_KNOWN_STATIC_FINALS = Collections.unmodifiableMap(values); + } + + private final Map fieldValues = new HashMap(); + + private final Map staticFinals = new HashMap(); + + @Override + public void visitVariable(VariableTree variable) throws Exception { + Set flags = variable.getModifierFlags(); + if (flags.contains(Modifier.STATIC) && flags.contains(Modifier.FINAL)) { + this.staticFinals.put(variable.getName(), getValue(variable)); + } + if (!flags.contains(Modifier.FINAL)) { + this.fieldValues.put(variable.getName(), getValue(variable)); + } + } + + private Object getValue(VariableTree variable) throws Exception { + ExpressionTree initializer = variable.getInitializer(); + Class wrapperType = WRAPPER_TYPES.get(variable.getType()); + if (initializer != null) { + if (initializer.getLiteralValue() != null) { + return initializer.getLiteralValue(); + } + if (initializer.getKind().equals("IDENTIFIER")) { + return this.staticFinals.get(initializer.toString()); + } + if (initializer.getKind().equals("MEMBER_SELECT")) { + return WELL_KNOWN_STATIC_FINALS.get(initializer.toString()); + } + } + return DEFAULT_TYPE_VALUES.get(wrapperType); + } + + public Map getFieldValues() { + return this.fieldValues; + } + + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java new file mode 100644 index 0000000000..f4f3b19101 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.fieldvalues.javac; + +import java.lang.reflect.Method; + +/** + * Base class for reflection based wrappers. Used to access internal Java classes without + * needing tools.jar on the classpath. + * + * @author Phillip Webb + * @since 1.2.0 + */ +class ReflectionWrapper { + + private final Class type; + + private final Object instance; + + public ReflectionWrapper(Object instance) { + this.type = instance.getClass(); + this.instance = instance; + } + + public ReflectionWrapper(String type, Object instance) { + this.type = findClass(instance.getClass().getClassLoader(), type); + this.instance = instance; + } + + protected final Object getInstance() { + return this.instance; + } + + @Override + public String toString() { + return this.instance.toString(); + } + + protected Class findClass(String name) { + return findClass(getInstance().getClass().getClassLoader(), name); + } + + protected Method findMethod(String name, Class... parameterTypes) { + return findMethod(this.type, name, parameterTypes); + } + + protected static Class findClass(ClassLoader classLoader, String name) { + try { + return classLoader.loadClass(name); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException(ex); + } + } + + protected static Method findMethod(Class type, String name, + Class... parameterTypes) { + try { + return type.getMethod(name, parameterTypes); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Tree.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Tree.java new file mode 100644 index 0000000000..86c197f762 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Tree.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.fieldvalues.javac; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +/** + * Reflection based access to {@code com.sun.source.tree.Tree}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +class Tree extends ReflectionWrapper { + + private final Class treeVisitorType = findClass("com.sun.source.tree.TreeVisitor"); + + private final Method acceptMethod = findMethod("accept", this.treeVisitorType, + Object.class); + + private final Method GET_CLASS_TREE_MEMBERS = findMethod( + findClass("com.sun.source.tree.ClassTree"), "getMembers"); + + public Tree(Object instance) { + super("com.sun.source.tree.Tree", instance); + } + + public void accept(TreeVisitor visitor) throws Exception { + this.acceptMethod.invoke(getInstance(), Proxy.newProxyInstance(getInstance() + .getClass().getClassLoader(), new Class[] { this.treeVisitorType }, + new TreeVisitorInvocationHandler(visitor)), 0); + } + + /** + * {@link InvocationHandler} to call the {@link TreeVisitor}. + */ + private class TreeVisitorInvocationHandler implements InvocationHandler { + + private TreeVisitor treeVisitor; + + public TreeVisitorInvocationHandler(TreeVisitor treeVisitor) { + this.treeVisitor = treeVisitor; + } + + @Override + @SuppressWarnings("rawtypes") + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("visitClass")) { + if ((Integer) args[1] == 0) { + Iterable members = (Iterable) Tree.this.GET_CLASS_TREE_MEMBERS + .invoke(args[0]); + for (Object member : members) { + if (member != null) { + Tree.this.acceptMethod.invoke(member, proxy, + ((Integer) args[1]) + 1); + } + } + } + } + if (method.getName().equals("visitVariable")) { + this.treeVisitor.visitVariable(new VariableTree(args[0])); + } + return null; + } + + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/TreeVisitor.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/TreeVisitor.java new file mode 100644 index 0000000000..b73cbc28d5 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/TreeVisitor.java @@ -0,0 +1,29 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.fieldvalues.javac; + +/** + * Reflection base alternative for {@code com.sun.source.tree.TreeVisitor}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +interface TreeVisitor { + + void visitVariable(VariableTree variable) throws Exception; + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java new file mode 100644 index 0000000000..639ecfc184 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.fieldvalues.javac; + +import java.lang.reflect.Method; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; + +/** + * Reflection based access to {@code com.sun.source.util.Trees}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +class Trees extends ReflectionWrapper { + + private Trees(Object instance) { + super(instance); + } + + public Tree getTree(Element element) throws Exception { + Object tree = findMethod("getTree", Element.class).invoke(getInstance(), element); + return (tree == null ? null : new Tree(tree)); + } + + public static Trees instance(ProcessingEnvironment env) throws Exception { + ClassLoader classLoader = env.getClass().getClassLoader(); + Class type = findClass(classLoader, "com.sun.source.util.Trees"); + Method method = findMethod(type, "instance", ProcessingEnvironment.class); + return new Trees(method.invoke(null, env)); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/VariableTree.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/VariableTree.java new file mode 100644 index 0000000000..4817a4a78e --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/VariableTree.java @@ -0,0 +1,59 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.fieldvalues.javac; + +import java.util.Collections; +import java.util.Set; + +import javax.lang.model.element.Modifier; + +/** + * Reflection based access to {@code com.sun.source.tree.VariableTree}. + * + * @author Phillip Webb + * @since 1.2.0 + */ +class VariableTree extends ReflectionWrapper { + + public VariableTree(Object instance) { + super(instance); + } + + public String getName() throws Exception { + return findMethod("getName").invoke(getInstance()).toString(); + } + + public String getType() throws Exception { + return findMethod("getType").invoke(getInstance()).toString(); + } + + public ExpressionTree getInitializer() throws Exception { + Object instance = findMethod("getInitializer").invoke(getInstance()); + return (instance == null ? null : new ExpressionTree(instance)); + } + + @SuppressWarnings("unchecked") + public Set getModifierFlags() throws Exception { + Object modifiers = findMethod("getModifiers").invoke(getInstance()); + if (modifiers == null) { + return Collections.emptySet(); + } + return (Set) findMethod(findClass("com.sun.source.tree.ModifiersTree"), + "getFlags").invoke(modifiers); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java index e0b4fd309f..f261f4a9b5 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java @@ -38,8 +38,11 @@ public class ItemMetadata implements Comparable { private String sourceMethod; + private Object defaultValue; + ItemMetadata(ItemType itemType, String prefix, String name, String type, - String sourceType, String sourceMethod, String description) { + String sourceType, String sourceMethod, String description, + Object defaultValue) { super(); this.itemType = itemType; this.name = buildName(prefix, name); @@ -47,6 +50,7 @@ public class ItemMetadata implements Comparable { this.sourceType = sourceType; this.sourceMethod = sourceMethod; this.description = description; + this.defaultValue = defaultValue; } private String buildName(String prefix, String name) { @@ -85,12 +89,17 @@ public class ItemMetadata implements Comparable { return this.description; } + public Object getDefaultValue() { + return this.defaultValue; + } + @Override public String toString() { StringBuilder string = new StringBuilder(this.name); buildToStringProperty(string, "type", this.type); buildToStringProperty(string, "sourceType", this.sourceType); buildToStringProperty(string, "description", this.description); + buildToStringProperty(string, "defaultValue", this.defaultValue); return string.toString(); } @@ -109,13 +118,14 @@ public class ItemMetadata implements Comparable { public static ItemMetadata newGroup(String name, String type, String sourceType, String sourceMethod) { return new ItemMetadata(ItemType.GROUP, name, null, type, sourceType, - sourceMethod, null); + sourceMethod, null, null); } public static ItemMetadata newProperty(String prefix, String name, String type, - String sourceType, String sourceMethod, String description) { + String sourceType, String sourceMethod, String description, + Object defaultValue) { return new ItemMetadata(ItemType.PROPERTY, prefix, name, type, sourceType, - sourceMethod, description); + sourceMethod, description, defaultValue); } /** diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java index 7d6f345996..c05f7b6156 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java @@ -67,6 +67,7 @@ public class JsonMarshaller { putIfPresent(jsonObject, "description", item.getDescription()); putIfPresent(jsonObject, "sourceType", item.getSourceType()); putIfPresent(jsonObject, "sourceMethod", item.getSourceMethod()); + putIfPresent(jsonObject, "defaultValue", item.getDefaultValue()); return jsonObject; } @@ -101,8 +102,9 @@ public class JsonMarshaller { String description = object.optString("description", null); String sourceType = object.optString("sourceType", null); String sourceMethod = object.optString("sourceMethod", null); + Object defaultValue = object.opt("defaultValue"); return new ItemMetadata(itemType, name, null, type, sourceType, sourceMethod, - description); + description, defaultValue); } private String toString(InputStream inputStream) throws IOException { diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index f90af42742..023692e7e1 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -71,9 +71,10 @@ public class ConfigurationMetadataAnnotationProcessorTests { assertThat(metadata, containsGroup("simple").fromSource(SimpleProperties.class)); assertThat( metadata, - containsProperty("simple.the-name", String.class).fromSource( - SimpleProperties.class).withDescription( - "The name of this simple properties.")); + containsProperty("simple.the-name", String.class) + .fromSource(SimpleProperties.class) + .withDescription("The name of this simple properties.") + .withDefaultValue("boot")); assertThat( metadata, containsProperty("simple.flag", Boolean.class).fromSource( @@ -171,7 +172,8 @@ public class ConfigurationMetadataAnnotationProcessorTests { @Test public void simpleMethodConfig() throws Exception { ConfigurationMetadata metadata = compile(SimpleMethodConfig.class); - assertThat(metadata, containsGroup("foo").fromSource(SimpleMethodConfig.class)); + assertThat(metadata, containsGroup("foo") + .fromSource(SimpleMethodConfig.class)); assertThat( metadata, containsProperty("foo.name", String.class).fromSource( diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java index c796172c13..0ffe042db4 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java @@ -66,17 +66,20 @@ public class ConfigurationMetadataMatchers { private final String description; + private final Object defaultValue; + public ContainsItemMatcher(ItemType itemType, String name) { - this(itemType, name, null, null, null); + this(itemType, name, null, null, null, null); } public ContainsItemMatcher(ItemType itemType, String name, String type, - Class sourceType, String description) { + Class sourceType, String description, Object defaultValue) { this.itemType = itemType; this.name = name; this.type = type; this.sourceType = sourceType; this.description = description; + this.defaultValue = defaultValue; } @Override @@ -93,6 +96,10 @@ public class ConfigurationMetadataMatchers { && !this.sourceType.getName().equals(itemMetadata.getSourceType())) { return false; } + if (this.defaultValue != null + && !this.defaultValue.equals(itemMetadata.getDefaultValue())) { + return false; + } if (this.description != null && !this.description.equals(itemMetadata.getDescription())) { return false; @@ -121,6 +128,9 @@ public class ConfigurationMetadataMatchers { if (this.sourceType != null) { description.appendText(" sourceType ").appendValue(this.sourceType); } + if (this.defaultValue != null) { + description.appendText(" defaultValue ").appendValue(this.defaultValue); + } if (this.description != null) { description.appendText(" description ").appendValue(this.description); } @@ -128,22 +138,27 @@ public class ConfigurationMetadataMatchers { public ContainsItemMatcher ofType(Class dataType) { return new ContainsItemMatcher(this.itemType, this.name, dataType.getName(), - this.sourceType, this.description); + this.sourceType, this.description, this.defaultValue); } public ContainsItemMatcher ofDataType(String dataType) { return new ContainsItemMatcher(this.itemType, this.name, dataType, - this.sourceType, this.description); + this.sourceType, this.description, this.defaultValue); } public ContainsItemMatcher fromSource(Class sourceType) { return new ContainsItemMatcher(this.itemType, this.name, this.type, - sourceType, this.description); + sourceType, this.description, this.defaultValue); } public ContainsItemMatcher withDescription(String description) { return new ContainsItemMatcher(this.itemType, this.name, this.type, - this.sourceType, description); + this.sourceType, description, this.defaultValue); + } + + public Matcher withDefaultValue(Object defaultValue) { + return new ContainsItemMatcher(this.itemType, this.name, this.type, + this.sourceType, this.description, defaultValue); } private ItemMetadata getFirstPropertyWithName(ConfigurationMetadata metadata, diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java new file mode 100644 index 0000000000..7cd2fa2e1e --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.fieldvalues; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +import org.hamcrest.Matcher; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.springframework.boot.configurationprocessor.TestCompiler; +import org.springframework.boot.configurationsample.fieldvalues.FieldValues; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * Abstract base class for {@link FieldValuesParser} tests. + * + * @author Phillip Webb + */ +public abstract class AbstractFieldValuesProcessorTests { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + protected abstract FieldValuesParser createProcessor(ProcessingEnvironment env); + + @Test + public void getFieldValues() throws Exception { + TestProcessor processor = new TestProcessor(); + TestCompiler compiler = new TestCompiler(this.temporaryFolder); + compiler.getTask(FieldValues.class).call(processor); + Map values = processor.getValues(); + assertThat(values.get("string"), equalToObject("1")); + assertThat(values.get("stringNone"), nullValue()); + assertThat(values.get("stringConst"), equalToObject("c")); + assertThat(values.get("bool"), equalToObject(true)); + assertThat(values.get("boolNone"), equalToObject(false)); + assertThat(values.get("boolConst"), equalToObject(true)); + assertThat(values.get("boolObject"), equalToObject(true)); + assertThat(values.get("boolObjectNone"), nullValue()); + assertThat(values.get("boolObjectConst"), equalToObject(true)); + assertThat(values.get("integer"), equalToObject(1)); + assertThat(values.get("integerNone"), equalToObject(0)); + assertThat(values.get("integerConst"), equalToObject(2)); + assertThat(values.get("integerObject"), equalToObject(3)); + assertThat(values.get("integerObjectNone"), nullValue()); + assertThat(values.get("integerObjectConst"), equalToObject(4)); + assertThat(values.get("object"), equalToObject(123)); + assertThat(values.get("objectNone"), nullValue()); + assertThat(values.get("objectConst"), equalToObject("c")); + assertThat(values.get("objectInstance"), nullValue()); + } + + private Matcher equalToObject(Object object) { + return equalTo(object); + } + + @SupportedAnnotationTypes({ "org.springframework.boot.configurationsample.ConfigurationProperties" }) + @SupportedSourceVersion(SourceVersion.RELEASE_6) + private class TestProcessor extends AbstractProcessor { + + private FieldValuesParser processor; + + private Map values = new HashMap(); + + @Override + public synchronized void init(ProcessingEnvironment env) { + this.processor = createProcessor(env); + } + + @Override + public boolean process(Set annotations, + RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) { + if (element instanceof TypeElement) { + try { + this.values.putAll(this.processor + .getFieldValues((TypeElement) element)); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + } + } + return false; + } + + public Map getValues() { + return this.values; + } + + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesProcessorTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesProcessorTests.java new file mode 100644 index 0000000000..bc1424ac74 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesProcessorTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2012-2014 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.configurationprocessor.fieldvalues.javac; + +import javax.annotation.processing.ProcessingEnvironment; + +import org.springframework.boot.configurationprocessor.fieldvalues.AbstractFieldValuesProcessorTests; +import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser; + +import static org.junit.Assume.assumeNoException; + +/** + * Tests for {@link JavaCompilerFieldValuesParser}. + * + * @author Phillip Webb + */ +public class JavaCompilerFieldValuesProcessorTests extends + AbstractFieldValuesProcessorTests { + + @Override + protected FieldValuesParser createProcessor(ProcessingEnvironment env) { + try { + return new JavaCompilerFieldValuesParser(env); + } + catch (Throwable ex) { + assumeNoException(ex); + throw new IllegalStateException(); + } + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java index d7cf0efa2c..76a04631ff 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java @@ -38,10 +38,11 @@ public class JsonMarshallerTests { public void marshallAndUnmarshal() throws IOException { ConfigurationMetadata metadata = new ConfigurationMetadata(); metadata.add(ItemMetadata.newProperty("a", "b", StringBuffer.class.getName(), - InputStream.class.getName(), "sourceMethod", "desc")); - metadata.add(ItemMetadata.newProperty("b.c.d", null, null, null, null, null)); - metadata.add(ItemMetadata.newProperty("c", null, null, null, null, null)); - metadata.add(ItemMetadata.newProperty("d", null, null, null, null, null)); + InputStream.class.getName(), "sourceMethod", "desc", "x")); + metadata.add(ItemMetadata + .newProperty("b.c.d", null, null, null, null, null, null)); + metadata.add(ItemMetadata.newProperty("c", null, null, null, null, null, 123)); + metadata.add(ItemMetadata.newProperty("d", null, null, null, null, null, true)); metadata.add(ItemMetadata.newGroup("d", null, null, null)); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); JsonMarshaller marshaller = new JsonMarshaller(); @@ -51,10 +52,10 @@ public class JsonMarshallerTests { outputStream.toByteArray())); assertThat(read, containsProperty("a.b", StringBuffer.class).fromSource(InputStream.class) - .withDescription("desc")); + .withDescription("desc").withDefaultValue("x")); assertThat(read, containsProperty("b.c.d")); - assertThat(read, containsProperty("c")); - assertThat(read, containsProperty("d")); + assertThat(read, containsProperty("c").withDefaultValue(123)); + assertThat(read, containsProperty("d").withDefaultValue(true)); assertThat(read, containsGroup("d")); } diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/FieldValues.class b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/fieldvalues/FieldValues.class new file mode 100644 index 0000000000000000000000000000000000000000..4b15ca5a34e7747e03445d399762031fd5e588be GIT binary patch literal 1603 zcmb7EZFAE`5PmKRmZK;yHYFv5riB#h2pSif7D&>xapI&-jh)&ig+Kw>kv*c0l#yll z&_C3#z<6NVnf`$OD5lSy&avrCKQNlvy=QlCZ|~VX{qye6zX7b^$iy7BlgN)_3_AvP zllW0C3sUS!@zXepxGPsBDaumpOYyT56%$p|4%-quG@0m; zfnNozYOPWz?LOHmm#Q^^bn%(<+{v~bzm=^O4^(DOKzrmrLz6<+@yd<~AaMsR4*1rw0ZdZ+P+HJVmeK zNJ}FfHmPAhbLu@DZ%Myr(;E|ku4T9jaTea!jJ@&xi5TdOQ!y&2E;YzPV%_t-XhUE$ zvvfcjwt^#Cr;DENmb%AvH>^4JHZ95ZMq5Xk)u0H+?^doXF#32<#Puu$Aqc$a!go&awL<2!G3xzbM-X{HE9HhHJRHJMhJBrCn5HK|sZY%6_RYZ9+8DOOs@d11xh5&eNt zu7F4O8wfi+(!;2oKGQ?O9_zu7B5A+Ecn>DkL=ToUP1=l@vJ-DH-NOtWsUFhy% Date: Tue, 28 Oct 2014 15:04:52 -0700 Subject: [PATCH 3/5] Generate standard configuration meta-data Update the `spring-boot`, `spring-boot-autoconfigure` and `spring-boot-actuator` project to generate configuration meta-data files during compilation. See gh-1001 --- spring-boot-actuator/pom.xml | 6 ++ ...itional-spring-configuration-metadata.json | 8 ++ spring-boot-autoconfigure/pom.xml | 6 ++ .../jdbc/DataSourceConfigMetadata.java | 49 +++++++++++ .../jdbc/DataSourceProperties.java | 2 +- .../autoconfigure/web/ServerProperties.java | 1 + ...itional-spring-configuration-metadata.json | 85 +++++++++++++++++++ spring-boot/pom.xml | 6 ++ ...itional-spring-configuration-metadata.json | 73 ++++++++++++++++ 9 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfigMetadata.java create mode 100644 spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index f9398c0b25..5adccd08d7 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -151,6 +151,12 @@ commons-dbcp true + + + org.springframework.boot + spring-boot-configuration-processor + true + ch.qos.logback diff --git a/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000000..e34f94ad4d --- /dev/null +++ b/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,8 @@ +{"properties": [ + { + "name": "spring.git.properties", + "dataType": "java.lang.String", + "description": "Resource reference to a generated git info properties file." + } +]} + diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index 61d4b3b820..32f4afcab9 100644 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -390,6 +390,12 @@ aspectjweaver true + + + org.springframework.boot + spring-boot-configuration-processor + true + org.springframework.boot diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfigMetadata.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfigMetadata.java new file mode 100644 index 0000000000..d7faf1c493 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfigMetadata.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2014 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.autoconfigure.jdbc; + +import com.zaxxer.hikari.HikariDataSource; +import org.apache.commons.dbcp.BasicDataSource; +import org.apache.tomcat.jdbc.pool.DataSource; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Expose the metadata of the supported data sources. Only used to harvest + * the relevant properties metadata. + * + * @author Stephane Nicoll + * @since 1.2.0 + */ +class DataSourceConfigMetadata { + + @ConfigurationProperties(DataSourceProperties.PREFIX) + public DataSource tomcatDataSource() { + return (DataSource) DataSourceBuilder.create().type(DataSource.class).build(); + } + + @ConfigurationProperties(DataSourceProperties.PREFIX) + public HikariDataSource hikariDataSource() { + return (HikariDataSource) DataSourceBuilder.create().type(HikariDataSource.class).build(); + } + + @ConfigurationProperties(DataSourceProperties.PREFIX) + public BasicDataSource dbcpDataSource() { + return (BasicDataSource)DataSourceBuilder.create().type(BasicDataSource.class).build(); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java index fb8ee46c7e..1a752d4210 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java @@ -80,7 +80,7 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB .get(this.classLoader); } - protected String getDriverClassName() { + public String getDriverClassName() { if (StringUtils.hasText(this.driverClassName)) { Assert.state(ClassUtils.isPresent(this.driverClassName, null), "Cannot load driver class: " + this.driverClassName); diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 421e24488c..5a1a82c17b 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -381,4 +381,5 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { } } + } diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000000..2d0c34d1ea --- /dev/null +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,85 @@ +{"properties": [ + { + "name": "spring.aop.auto", + "dataType": "java.lang.Boolean", + "description": "Automatically adds @EnableAspectJAutoProxy.", + "defaultValue": true, + }, + { + "name": "spring.aop.proxy-target-class", + "dataType": "java.lang.Boolean", + "description": "Whether subclass-based (CGLIB) proxies are to be created (true) as opposed to standard Java interface-based proxies (false).", + "defaultValue": false, + }, + { + "name": "spring.batch.enabled", + "dataType": "java.lang.Boolean", + "description": "Execute all Spring Batch jobs in the context on startup.", + "defaultValue": true, + }, + { + "name": "spring.data.elasticsearch.repositories.enabled", + "dataType": "java.lang.Boolean", + "description": "Automatically enable Elasticsearch repositories.", + "defaultValue": true, + }, + { + "name": "spring.data.jpa.repositories.enabled", + "dataType": "java.lang.Boolean", + "description": "Automatically enable JPA repositories.", + "defaultValue": true, + }, + { + "name": "spring.data.mongo.repositories.enabled", + "dataType": "java.lang.Boolean", + "description": "Automatically enable Mongo repositories.", + "defaultValue": true, + }, + { + "name": "spring.data.solr.repositories.enabled", + "dataType": "java.lang.Boolean", + "description": "Automatically enable Solr repositories.", + "defaultValue": true, + }, + { + "name": "spring.jmx.enabled", + "dataType": "java.lang.Boolean", + "description": "Automatically expose management beans to the JMX domain", + "defaultValue": true, + }, + { + "name": "spring.jpa.open-in-view", + "dataType": "java.lang.Boolean", + "description": "Automatically register OpenEntityManagerInViewInterceptor. Binds a JPA EntityManager to the thread for the entire processing of the request.", + "defaultValue": true, + }, + { + "name": "spring.mobile.devicedelegatingviewresolver.enabled", + "dataType": "java.lang.Boolean", + "description": "Enable device view resolver.", + "defaultValue": false, + }, + { + "name": "spring.mobile.sitepreference.enabled", + "dataType": "java.lang.Boolean", + "description": "Enable SitePreferenceHandler.", + "defaultValue": true, + }, + { + "name": "spring.social.auto-connection-views", + "dataType": "java.lang.Boolean", + "description": "Automatically enable the connection status view for supported providers.", + "defaultValue": false, + }, + { + "name": "spring.view.prefix", + "dataType": "java.lang.String", + "description": "Spring MVC view prefix.", + }, + { + "name": "spring.view.suffix", + "dataType": "java.lang.String", + "description": "Spring MVC view suffix.", + } +]} + diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml index d51218d1f2..fa1b23f683 100644 --- a/spring-boot/pom.xml +++ b/spring-boot/pom.xml @@ -169,6 +169,12 @@ snakeyaml true + + + org.springframework.boot + spring-boot-configuration-processor + true + org.apache.httpcomponents diff --git a/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000000..526c4af3fa --- /dev/null +++ b/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,73 @@ +{"groups": [ + { + "name": "logging", + "sourceType": "org.springframework.boot.logging.LoggingApplicationListener" + } +],"properties": [ + { + "name": "logging.config", + "dataType": "java.lang.String", + "description": "Location of the logging configuration file." + }, + { + "name": "logging.file", + "dataType": "java.lang.String", + "description": "The name of the log file." + }, + { + "name": "logging.path", + "dataType": "java.lang.String", + "description": "Location of the log file." + }, + { + "name": "spring.mandatory-file-encoding", + "sourceType": "org.springframework.boot.context.FileEncodingApplicationListener", + "dataType": "java.lang.String", + "description": "The character encoding the application must use." + }, + { + "name": "spring.application.name", + "dataType": "java.lang.String", + "sourceType": "org.springframework.boot.context.ContextIdApplicationContextInitializer", + "description": "The name of the application." + }, + { + "name": "spring.application.index", + "dataType": "java.lang.Integer", + "sourceType": "org.springframework.boot.context.ContextIdApplicationContextInitializer", + "description": "Index of the application." + }, + { + "name": "spring.config.name", + "dataType": "java.lang.String", + "sourceType": "org.springframework.boot.context.config.ConfigFileApplicationListener", + "description": "Config file name", + "defaultValue": "application", + }, + { + "name": "spring.config.location", + "dataType": "java.lang.String", + "sourceType": "org.springframework.boot.context.config.ConfigFileApplicationListener", + "description": "Config file locations", + }, + { + "name": "spring.main.show-banner", + "dataType": "java.lang.Boolean", + "sourceType": "org.springframework.boot.SpringApplication", + "description": "Display the banner when the application runs", + "defaultValue": true, + }, + { + "name": "spring.main.sources", + "dataType": "java.util.Set", + "sourceType": "org.springframework.boot.SpringApplication", + "description": "Sources (class name, package name or XML resource location) used to create the ApplicationContext." + }, + { + "name": "spring.main.web-environment", + "dataType": "java.lang.Boolean", + "sourceType": "org.springframework.boot.SpringApplication", + "description": "Run the application in a web environment (auto-detected by default)" + } +]} + From a46396f691a238cdbfa7f50ac31123ed92777e3f Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 31 Oct 2014 10:09:33 -0700 Subject: [PATCH 4/5] Add @NestedConfigurationProperty meta-data support Add a @NestedConfigurationProperty annotation which can be used to customize how configuration mete-data is generated. Prior to this commit only inner-classes where considered nested (see Tomcat in ServerProperties). Using this new annotation, the Ssl property in ServerProperties can be detected as well. See gh-1001 --- .../autoconfigure/web/ServerProperties.java | 2 + ...figurationMetadataAnnotationProcessor.java | 32 +++++++++++---- ...ationMetadataAnnotationProcessorTests.java | 15 ++++--- .../NestedConfigurationProperty.java | 38 ++++++++++++++++++ .../specific/InnerClassProperties.java | 7 +--- .../NestedConfigurationProperty.java | 40 +++++++++++++++++++ 6 files changed, 116 insertions(+), 18 deletions(-) create mode 100644 spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 5a1a82c17b..f8d5a0335c 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -38,6 +38,7 @@ import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomize import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; import org.springframework.util.StringUtils; /** @@ -60,6 +61,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer { private String contextPath; + @NestedConfigurationProperty private Ssl ssl; @NotNull diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index f2a08639d4..371bbd2ad1 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -64,6 +64,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot." + "context.properties.ConfigurationProperties"; + static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot." + + "context.properties.NestedConfigurationProperty"; + private ConfigurationMetadata metadata; private TypeUtils typeUtils; @@ -74,6 +77,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor return CONFIGURATION_PROPERTIES_ANNOTATION; } + protected String nestedConfigurationPropertyAnnotation() { + return NESTED_CONFIGURATION_PROPERTY_ANNOTATION; + } + @Override public synchronized void init(ProcessingEnvironment env) { super.init(env); @@ -166,8 +173,11 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor ExecutableElement getter = entry.getValue(); ExecutableElement setter = members.getPublicSetters().get(name); VariableElement field = members.getFields().get(name); - if (setter != null - || this.typeUtils.isCollectionOrMap(getter.getReturnType())) { + boolean isNested = getAnnotation(field, + nestedConfigurationPropertyAnnotation()) != null; + boolean isCollection = this.typeUtils.isCollectionOrMap(getter + .getReturnType()); + if (!isNested && (setter != null || isCollection)) { String dataType = this.typeUtils.getType(getter.getReturnType()); String sourceType = this.typeUtils.getType(element); String description = this.typeUtils.getJavaDoc(field); @@ -182,17 +192,21 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor TypeElementMembers members) { for (Map.Entry entry : members.getPublicGetters() .entrySet()) { + String name = entry.getKey(); ExecutableElement getter = entry.getValue(); + VariableElement field = members.getFields().get(name); Element returnType = this.processingEnv.getTypeUtils().asElement( getter.getReturnType()); AnnotationMirror annotation = getAnnotation(getter, configurationPropertiesAnnotation()); + boolean isNested = getAnnotation(field, + nestedConfigurationPropertyAnnotation()) != null; if (returnType != null && returnType instanceof TypeElement && annotation == null) { TypeElement returns = (TypeElement) returnType; - if (this.typeUtils.isEnclosedIn(returnType, element)) { - String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, - entry.getKey()); + if (this.typeUtils.isEnclosedIn(returnType, element) || isNested) { + String nestedPrefix = ConfigurationMetadata + .nestedPrefix(prefix, name); this.metadata.add(ItemMetadata.newGroup(nestedPrefix, this.typeUtils.getType(returns), this.typeUtils.getType(element), getter.toString())); @@ -203,9 +217,11 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor } private AnnotationMirror getAnnotation(Element element, String type) { - for (AnnotationMirror annotation : element.getAnnotationMirrors()) { - if (type.equals(annotation.getAnnotationType().toString())) { - return annotation; + if (element != null) { + for (AnnotationMirror annotation : element.getAnnotationMirrors()) { + if (type.equals(annotation.getAnnotationType().toString())) { + return annotation; + } } } return null; diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index 023692e7e1..e4d8ffb0de 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -39,6 +39,7 @@ import org.springframework.boot.configurationsample.simple.SimpleTypeProperties; import org.springframework.boot.configurationsample.specific.InnerClassAnnotatedGetterConfig; import org.springframework.boot.configurationsample.specific.InnerClassProperties; import org.springframework.boot.configurationsample.specific.InnerClassRootConfig; +import org.springframework.boot.configurationsample.specific.SimplePojo; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; @@ -238,11 +239,8 @@ public class ConfigurationMetadataAnnotationProcessorTests { .fromSource(InnerClassProperties.class)); assertThat(metadata, containsProperty("config.the-second.name")); assertThat(metadata, containsProperty("config.the-second.bar.name")); - assertThat( - metadata, - containsGroup("config.third").ofType( - InnerClassProperties.SimplePojo.class).fromSource( - InnerClassProperties.class)); + assertThat(metadata, containsGroup("config.third").ofType(SimplePojo.class) + .fromSource(InnerClassProperties.class)); assertThat(metadata, containsProperty("config.third.value")); } @@ -267,6 +265,8 @@ public class ConfigurationMetadataAnnotationProcessorTests { static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot.configurationsample.ConfigurationProperties"; + static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.configurationsample.NestedConfigurationProperty"; + private ConfigurationMetadata metadata; @Override @@ -274,6 +274,11 @@ public class ConfigurationMetadataAnnotationProcessorTests { return CONFIGURATION_PROPERTIES_ANNOTATION; } + @Override + protected String nestedConfigurationPropertyAnnotation() { + return NESTED_CONFIGURATION_PROPERTY_ANNOTATION; + } + @Override protected void writeMetaData(ConfigurationMetadata metadata) { this.metadata = metadata; diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java new file mode 100644 index 0000000000..8d2457bff0 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/NestedConfigurationProperty.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2014 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.configurationsample; + +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 @NestedConfigurationProperty} for testing (removes + * the need for a dependency on the real annotation). + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.2.0 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface NestedConfigurationProperty { + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java index d9791a7571..ff03fb68f5 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/specific/InnerClassProperties.java @@ -17,6 +17,7 @@ package org.springframework.boot.configurationsample.specific; import org.springframework.boot.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.NestedConfigurationProperty; /** * Demonstrate the auto-detection of a inner config classes. @@ -30,6 +31,7 @@ public class InnerClassProperties { private Foo second = new Foo(); + @NestedConfigurationProperty private final SimplePojo third = new SimplePojo(); public Foo getFirst() { @@ -81,9 +83,4 @@ public class InnerClassProperties { } - public static class SimplePojo extends - org.springframework.boot.configurationsample.specific.SimplePojo { - - } - } diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java new file mode 100644 index 0000000000..ebb78b3491 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2014 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.context.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; + +/** + * Indicates that a field in a {@link ConfigurationProperties} object should be treated as + * if it were a nested type. This annotation has no bearing on the actual binding + * processes, but it is used by the {@code spring-boot-configuration-processor} as a hint + * that a field is not bound as a single value. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.2.0 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface NestedConfigurationProperty { + +} From 9d01d4fa84fc9f0388c3d2a5bf37f4ba8d844b3d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 2 Nov 2014 18:03:14 -0800 Subject: [PATCH 5/5] Document configuration meta-data Add an appendix to the reference documentation describing the format of configuration meta-data and how the annotation processor can be used. Closes gh-1001 --- .../appendix-configuration-metadata.adoc | 234 ++++++++++++++++++ .../src/main/asciidoc/appendix.adoc | 1 + 2 files changed, 235 insertions(+) create mode 100644 spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc diff --git a/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc b/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc new file mode 100644 index 0000000000..2ac40ca15d --- /dev/null +++ b/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc @@ -0,0 +1,234 @@ +[appendix] +[[configuration-metadata]] +== Configuration meta-data +Spring Boot jars are shipped with meta-data files that provide details of all supported +configuration properties. The files are designed to allow IDE developers to offer +contextual help and "`code completion`" as users are working with `application.properies` +or `application.yml` files. + +The majority of the meta-data file is generated automatically at compile time by +processing all items annotated with `@ConfigurationProperties`. + + + +[[configuration-metadata-format]] +=== Meta-data format +Configuration meta-data files are located inside jars under +`META-INF/spring-configuration-metadata.json` They use a simple JSON format with items +categorized under either "`groups`" or "`properties`": + +[source,json,indent=0] +---- + {"groups": [ + { + "name": "server", + "type": "org.springframework.boot.autoconfigure.web.ServerProperties", + "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties" + } + ... + ],"properties": [ + { + "name": "server.port", + "type": "java.lang.Integer", + "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties" + }, + { + "name": "server.servlet-path", + "type": "java.lang.String", + "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties" + "defaultValue": "/" + } + ... + ]} +---- + +Each "`property`" is a configuration item that the user specifies with a given value. +For example `server.port` and `server.servlet-path` might be specified in +`application.properties` as follows: + +[source,properties,indent=0] +---- + server.port=9090 + server.servlet-path=/home +---- + +The "`groups`" are higher level items that don't themselves specify a value, but instead +provide a contextual grouping for properties. For example the `server.port` and +`server.servlet-path` properties are part of the `server` group. + +NOTE: It is not required that every "`property`" has a "`group`", some properties might +just exist in their own right. + + + +[[configuration-metadata-group-attributes]] +==== Group Attributes +The JSON object contained in the `groups` array can contain the following attributes: + +[cols="1,1,4"] +|=== +|Name | Type |Purpose + +|`name` +| String +| The full name of the group. This attribute is mandatory. + +|`type` +| String +| The class name of the data type of the group. For example, if the group was based + on a class annotated with `@ConfigurationProperties` the attribute would contain the + fully qualified name of that class. If it was based on a `@Bean` method, it would be + the return type of that method. The attribute may be omitted if the type is not known. + +|`description` +| String +| A short description of the group that can be displayed to users. May be omitted if no + description is available. + +|`sourceType` +| String +| The class name of the source that contributed this group. For example, if the group + was based on a `@Bean` method annotated with `@ConfigurationProperties` this attribute + would contain the fully qualified name of the `@Configuration` class containing the + method. The attribute may be omitted if the source type is not known. + +|`sourceMethod` +| String +| The full name of the method (include parenthesis and argument types) that contributed + this group. For example, the name of a `@ConfigurationProperties` annotated `@Bean` + method. May be omitted if the source method is not known. +|=== + + + +[[configuration-metadata-property-attributes]] +==== Property Attributes +The JSON object contained in the `properties` array can contain the following attributes: + +[cols="1,1,4"] +|=== +|Name | Type |Purpose + +|`name` +| String +| The full name of the property. Names are in lowercase dashed form (e.g. + `server.servlet-path`). This attribute is mandatory. + +|`type` +| String +| The class name of the data type of the property. For example, `java.lang.String`. This + attribute can be used to guide the user as to the types of values that they can enter. + For consistency, the type of a primitive is specified using its wrapper counterpart, + i.e. `boolean` becomes `java.lang.Boolean`. Note that this class may be a complex type + that gets converted from a String as values are bound. May be omitted if the type is + not known. + +|`description` +| String +| A short description of the property that can be displayed to users. May be omitted if + no description is available. + +|`sourceType` +| String +| The class name of the source that contributed this property. For example, if the + property was from a class annotated with `@ConfigurationProperties` this attribute + would contain the fully qualified name of that class. May be omitted if the source type + is not known. + +|`sourceMethod` +| String +| The full name of the method (include parenthesis and argument types) that contributed + this property. For example, the name of a getter in a `@ConfigurationProperties` + annotated class. May be omitted if the source method is not known. + +|`defaultValue` +| Object +| The default value which will be used if the property is not specified. May be omitted + if the default value is not known. +|=== + + + +[[configuration-metadata-repeated-items]] +==== Repeated meta-data items +It is perfectly acceptable for "`property`" and "`group`" objects with the same name to +appear multiple times within a meta-data file. For example, Spring Boot binds +`spring.datasource` properties to Hikari, Tomcat and DBCP classes, with each potentially +offering overlap of property names. Consumers of meta-data should take care to ensure +that they support such scenarios. + + + +[[configuration-metadata-annotation-processor]] +=== Generating your own meta-data using the annotation processor +You can easily generate your own configuration meta-data file from items annotated with +`@ConfigurationProperties` by using the `spring-boot-configuration-processor` jar. +The jar includes a Java annotation processor which is invoked as your project is +compiled. To use the processor, simply include `spring-boot-configuration-processor` as +an optional dependency, for example with Maven you would add: + +[source,xml,indent=0,subs="verbatim,quotes,attributes"] +---- + + org.springframework.boot + spring-boot-configuration-processor + true + +---- + +The annotation will pickup both classes and methods that are annotated with +`@ConfigurationProperties`. The Javadoc for field values within configuration classes +will be used to populate the `description` attribute. + +NOTE: You should only use simple text with `@ConfigurationProperties` field Javadoc since +they are not processed before being added to the JSON. + + + +[[configuration-metadata-nested-properties]] +==== Nested properties +The annotation processor will automatically consider inner classes as nested properties. +For example, the following class: + +[source,java,indent=0,subs="verbatim,quotes,attributes"] +---- + @ConfigurationProperties(prefix="server") + public class ServerProperties { + + private String name; + + private Host host; + + // ... getter and setters + + private static class Host { + + private String ip; + + private int port; + + // ... getter and setters + + } + + } +---- + +Will produce meta-data information for `server.name`, `server.host.ip` and +`server.host.port` properties. You can use the `@NestedConfigurationProperty` +annotation on a field to indicate that a regular (non-inner) class should be treated as +if it were nested. + + + +[[configuration-metadata-additional-metadata]] +==== Adding additional meta-data +Spring Boot's configuration file handling is quite flexible; and it often the case that +properties may exist that are not bound to a `@ConfigurationProperties` bean. To support +such cases, the annotation processor will automatically merge items from +`META-INF/additional-spring-configuration-metadata.json` into the main meta-data file. + +The format of the `additional-spring-configuration-metadata.json` file is exactly the same +as the regular `spring-configuration-metadata.json`. The additional properties file is +optional, if you don't have any additional properties, simply don't add it. + diff --git a/spring-boot-docs/src/main/asciidoc/appendix.adoc b/spring-boot-docs/src/main/asciidoc/appendix.adoc index 9767f8a6c6..ea8875ca7e 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix.adoc @@ -2,6 +2,7 @@ = Appendices include::appendix-application-properties.adoc[] +include::appendix-configuration-metadata.adoc[] include::appendix-auto-configuration-classes.adoc[] include::appendix-executable-jar-format.adoc[] include::appendix-dependency-versions.adoc[]