From 884c058e573ff3bf3824aea414f56485d514d00d Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 31 Oct 2014 10:18:28 -0700 Subject: [PATCH] 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 111bcc6c83c..1ad9576456e 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 0275a7c49e8..cf48644065d 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 a21fbe528e3..6dfefa64f28 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 00000000000..71970bbe482 --- /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 00000000000..eaee1f5e5ee --- /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 00000000000..4a5832c3bd8 --- /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 00000000000..10789f8665f --- /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 00000000000..8d44ffe180b --- /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 00000000000..e0b4fd309f8 --- /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 00000000000..7d6f3459969 --- /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 00000000000..e01209af7cb --- /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 00000000000..f90af427425 --- /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 00000000000..c796172c139 --- /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 00000000000..b10d0ece5df --- /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 00000000000..d7cf0efa2cd --- /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 00000000000..1f53f555eaa --- /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 00000000000..2797be051ae --- /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 00000000000..adff4f45fe5 --- /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 00000000000..d59d120a739 --- /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 00000000000..93b6755d371 --- /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 00000000000..66211c885bf --- /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 00000000000..dc18e891098 --- /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 00000000000..0baf41aeb35 --- /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 00000000000..8346236a47a --- /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 00000000000..91d14019a28 --- /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 00000000000..284fb4afddd --- /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 00000000000..c5e4379f367 --- /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 00000000000..a6f7778df46 --- /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 00000000000..18cb060146a --- /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 00000000000..d9791a75715 --- /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 00000000000..6910b57f555 --- /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 00000000000..14fc86cd47c --- /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; + } + +}