diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml
index f9398c0b25..5adccd08d7 100644
--- a/spring-boot-actuator/pom.xml
+++ b/spring-boot-actuator/pom.xml
@@ -151,6 +151,12 @@
commons-dbcp
true
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
ch.qos.logback
diff --git a/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 0000000000..e34f94ad4d
--- /dev/null
+++ b/spring-boot-actuator/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,8 @@
+{"properties": [
+ {
+ "name": "spring.git.properties",
+ "dataType": "java.lang.String",
+ "description": "Resource reference to a generated git info properties file."
+ }
+]}
+
diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml
index 61d4b3b820..32f4afcab9 100644
--- a/spring-boot-autoconfigure/pom.xml
+++ b/spring-boot-autoconfigure/pom.xml
@@ -390,6 +390,12 @@
aspectjweaver
true
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
org.springframework.boot
diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfigMetadata.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfigMetadata.java
new file mode 100644
index 0000000000..d7faf1c493
--- /dev/null
+++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceConfigMetadata.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.autoconfigure.jdbc;
+
+import com.zaxxer.hikari.HikariDataSource;
+import org.apache.commons.dbcp.BasicDataSource;
+import org.apache.tomcat.jdbc.pool.DataSource;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Expose the metadata of the supported data sources. Only used to harvest
+ * the relevant properties metadata.
+ *
+ * @author Stephane Nicoll
+ * @since 1.2.0
+ */
+class DataSourceConfigMetadata {
+
+ @ConfigurationProperties(DataSourceProperties.PREFIX)
+ public DataSource tomcatDataSource() {
+ return (DataSource) DataSourceBuilder.create().type(DataSource.class).build();
+ }
+
+ @ConfigurationProperties(DataSourceProperties.PREFIX)
+ public HikariDataSource hikariDataSource() {
+ return (HikariDataSource) DataSourceBuilder.create().type(HikariDataSource.class).build();
+ }
+
+ @ConfigurationProperties(DataSourceProperties.PREFIX)
+ public BasicDataSource dbcpDataSource() {
+ return (BasicDataSource)DataSourceBuilder.create().type(BasicDataSource.class).build();
+ }
+
+}
diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java
index fb8ee46c7e..1a752d4210 100644
--- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java
+++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java
@@ -80,7 +80,7 @@ public class DataSourceProperties implements BeanClassLoaderAware, InitializingB
.get(this.classLoader);
}
- protected String getDriverClassName() {
+ public String getDriverClassName() {
if (StringUtils.hasText(this.driverClassName)) {
Assert.state(ClassUtils.isPresent(this.driverClassName, null),
"Cannot load driver class: " + this.driverClassName);
diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java
index 421e24488c..f8d5a0335c 100644
--- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java
+++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java
@@ -38,6 +38,7 @@ import org.springframework.boot.context.embedded.tomcat.TomcatConnectorCustomize
import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.springframework.util.StringUtils;
/**
@@ -60,6 +61,7 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer {
private String contextPath;
+ @NestedConfigurationProperty
private Ssl ssl;
@NotNull
@@ -381,4 +383,5 @@ public class ServerProperties implements EmbeddedServletContainerCustomizer {
}
}
+
}
diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 0000000000..2d0c34d1ea
--- /dev/null
+++ b/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,85 @@
+{"properties": [
+ {
+ "name": "spring.aop.auto",
+ "dataType": "java.lang.Boolean",
+ "description": "Automatically adds @EnableAspectJAutoProxy.",
+ "defaultValue": true,
+ },
+ {
+ "name": "spring.aop.proxy-target-class",
+ "dataType": "java.lang.Boolean",
+ "description": "Whether subclass-based (CGLIB) proxies are to be created (true) as opposed to standard Java interface-based proxies (false).",
+ "defaultValue": false,
+ },
+ {
+ "name": "spring.batch.enabled",
+ "dataType": "java.lang.Boolean",
+ "description": "Execute all Spring Batch jobs in the context on startup.",
+ "defaultValue": true,
+ },
+ {
+ "name": "spring.data.elasticsearch.repositories.enabled",
+ "dataType": "java.lang.Boolean",
+ "description": "Automatically enable Elasticsearch repositories.",
+ "defaultValue": true,
+ },
+ {
+ "name": "spring.data.jpa.repositories.enabled",
+ "dataType": "java.lang.Boolean",
+ "description": "Automatically enable JPA repositories.",
+ "defaultValue": true,
+ },
+ {
+ "name": "spring.data.mongo.repositories.enabled",
+ "dataType": "java.lang.Boolean",
+ "description": "Automatically enable Mongo repositories.",
+ "defaultValue": true,
+ },
+ {
+ "name": "spring.data.solr.repositories.enabled",
+ "dataType": "java.lang.Boolean",
+ "description": "Automatically enable Solr repositories.",
+ "defaultValue": true,
+ },
+ {
+ "name": "spring.jmx.enabled",
+ "dataType": "java.lang.Boolean",
+ "description": "Automatically expose management beans to the JMX domain",
+ "defaultValue": true,
+ },
+ {
+ "name": "spring.jpa.open-in-view",
+ "dataType": "java.lang.Boolean",
+ "description": "Automatically register OpenEntityManagerInViewInterceptor. Binds a JPA EntityManager to the thread for the entire processing of the request.",
+ "defaultValue": true,
+ },
+ {
+ "name": "spring.mobile.devicedelegatingviewresolver.enabled",
+ "dataType": "java.lang.Boolean",
+ "description": "Enable device view resolver.",
+ "defaultValue": false,
+ },
+ {
+ "name": "spring.mobile.sitepreference.enabled",
+ "dataType": "java.lang.Boolean",
+ "description": "Enable SitePreferenceHandler.",
+ "defaultValue": true,
+ },
+ {
+ "name": "spring.social.auto-connection-views",
+ "dataType": "java.lang.Boolean",
+ "description": "Automatically enable the connection status view for supported providers.",
+ "defaultValue": false,
+ },
+ {
+ "name": "spring.view.prefix",
+ "dataType": "java.lang.String",
+ "description": "Spring MVC view prefix.",
+ },
+ {
+ "name": "spring.view.suffix",
+ "dataType": "java.lang.String",
+ "description": "Spring MVC view suffix.",
+ }
+]}
+
diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml
index 111bcc6c83..1ad9576456 100644
--- a/spring-boot-dependencies/pom.xml
+++ b/spring-boot-dependencies/pom.xml
@@ -159,6 +159,11 @@
spring-boot-autoconfigure
1.2.0.BUILD-SNAPSHOT
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ 1.2.0.BUILD-SNAPSHOT
+
org.springframework.boot
spring-boot-dependency-tools
diff --git a/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc b/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc
new file mode 100644
index 0000000000..2ac40ca15d
--- /dev/null
+++ b/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc
@@ -0,0 +1,234 @@
+[appendix]
+[[configuration-metadata]]
+== Configuration meta-data
+Spring Boot jars are shipped with meta-data files that provide details of all supported
+configuration properties. The files are designed to allow IDE developers to offer
+contextual help and "`code completion`" as users are working with `application.properies`
+or `application.yml` files.
+
+The majority of the meta-data file is generated automatically at compile time by
+processing all items annotated with `@ConfigurationProperties`.
+
+
+
+[[configuration-metadata-format]]
+=== Meta-data format
+Configuration meta-data files are located inside jars under
+`META-INF/spring-configuration-metadata.json` They use a simple JSON format with items
+categorized under either "`groups`" or "`properties`":
+
+[source,json,indent=0]
+----
+ {"groups": [
+ {
+ "name": "server",
+ "type": "org.springframework.boot.autoconfigure.web.ServerProperties",
+ "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
+ }
+ ...
+ ],"properties": [
+ {
+ "name": "server.port",
+ "type": "java.lang.Integer",
+ "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
+ },
+ {
+ "name": "server.servlet-path",
+ "type": "java.lang.String",
+ "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
+ "defaultValue": "/"
+ }
+ ...
+ ]}
+----
+
+Each "`property`" is a configuration item that the user specifies with a given value.
+For example `server.port` and `server.servlet-path` might be specified in
+`application.properties` as follows:
+
+[source,properties,indent=0]
+----
+ server.port=9090
+ server.servlet-path=/home
+----
+
+The "`groups`" are higher level items that don't themselves specify a value, but instead
+provide a contextual grouping for properties. For example the `server.port` and
+`server.servlet-path` properties are part of the `server` group.
+
+NOTE: It is not required that every "`property`" has a "`group`", some properties might
+just exist in their own right.
+
+
+
+[[configuration-metadata-group-attributes]]
+==== Group Attributes
+The JSON object contained in the `groups` array can contain the following attributes:
+
+[cols="1,1,4"]
+|===
+|Name | Type |Purpose
+
+|`name`
+| String
+| The full name of the group. This attribute is mandatory.
+
+|`type`
+| String
+| The class name of the data type of the group. For example, if the group was based
+ on a class annotated with `@ConfigurationProperties` the attribute would contain the
+ fully qualified name of that class. If it was based on a `@Bean` method, it would be
+ the return type of that method. The attribute may be omitted if the type is not known.
+
+|`description`
+| String
+| A short description of the group that can be displayed to users. May be omitted if no
+ description is available.
+
+|`sourceType`
+| String
+| The class name of the source that contributed this group. For example, if the group
+ was based on a `@Bean` method annotated with `@ConfigurationProperties` this attribute
+ would contain the fully qualified name of the `@Configuration` class containing the
+ method. The attribute may be omitted if the source type is not known.
+
+|`sourceMethod`
+| String
+| The full name of the method (include parenthesis and argument types) that contributed
+ this group. For example, the name of a `@ConfigurationProperties` annotated `@Bean`
+ method. May be omitted if the source method is not known.
+|===
+
+
+
+[[configuration-metadata-property-attributes]]
+==== Property Attributes
+The JSON object contained in the `properties` array can contain the following attributes:
+
+[cols="1,1,4"]
+|===
+|Name | Type |Purpose
+
+|`name`
+| String
+| The full name of the property. Names are in lowercase dashed form (e.g.
+ `server.servlet-path`). This attribute is mandatory.
+
+|`type`
+| String
+| The class name of the data type of the property. For example, `java.lang.String`. This
+ attribute can be used to guide the user as to the types of values that they can enter.
+ For consistency, the type of a primitive is specified using its wrapper counterpart,
+ i.e. `boolean` becomes `java.lang.Boolean`. Note that this class may be a complex type
+ that gets converted from a String as values are bound. May be omitted if the type is
+ not known.
+
+|`description`
+| String
+| A short description of the property that can be displayed to users. May be omitted if
+ no description is available.
+
+|`sourceType`
+| String
+| The class name of the source that contributed this property. For example, if the
+ property was from a class annotated with `@ConfigurationProperties` this attribute
+ would contain the fully qualified name of that class. May be omitted if the source type
+ is not known.
+
+|`sourceMethod`
+| String
+| The full name of the method (include parenthesis and argument types) that contributed
+ this property. For example, the name of a getter in a `@ConfigurationProperties`
+ annotated class. May be omitted if the source method is not known.
+
+|`defaultValue`
+| Object
+| The default value which will be used if the property is not specified. May be omitted
+ if the default value is not known.
+|===
+
+
+
+[[configuration-metadata-repeated-items]]
+==== Repeated meta-data items
+It is perfectly acceptable for "`property`" and "`group`" objects with the same name to
+appear multiple times within a meta-data file. For example, Spring Boot binds
+`spring.datasource` properties to Hikari, Tomcat and DBCP classes, with each potentially
+offering overlap of property names. Consumers of meta-data should take care to ensure
+that they support such scenarios.
+
+
+
+[[configuration-metadata-annotation-processor]]
+=== Generating your own meta-data using the annotation processor
+You can easily generate your own configuration meta-data file from items annotated with
+`@ConfigurationProperties` by using the `spring-boot-configuration-processor` jar.
+The jar includes a Java annotation processor which is invoked as your project is
+compiled. To use the processor, simply include `spring-boot-configuration-processor` as
+an optional dependency, for example with Maven you would add:
+
+[source,xml,indent=0,subs="verbatim,quotes,attributes"]
+----
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+----
+
+The annotation will pickup both classes and methods that are annotated with
+`@ConfigurationProperties`. The Javadoc for field values within configuration classes
+will be used to populate the `description` attribute.
+
+NOTE: You should only use simple text with `@ConfigurationProperties` field Javadoc since
+they are not processed before being added to the JSON.
+
+
+
+[[configuration-metadata-nested-properties]]
+==== Nested properties
+The annotation processor will automatically consider inner classes as nested properties.
+For example, the following class:
+
+[source,java,indent=0,subs="verbatim,quotes,attributes"]
+----
+ @ConfigurationProperties(prefix="server")
+ public class ServerProperties {
+
+ private String name;
+
+ private Host host;
+
+ // ... getter and setters
+
+ private static class Host {
+
+ private String ip;
+
+ private int port;
+
+ // ... getter and setters
+
+ }
+
+ }
+----
+
+Will produce meta-data information for `server.name`, `server.host.ip` and
+`server.host.port` properties. You can use the `@NestedConfigurationProperty`
+annotation on a field to indicate that a regular (non-inner) class should be treated as
+if it were nested.
+
+
+
+[[configuration-metadata-additional-metadata]]
+==== Adding additional meta-data
+Spring Boot's configuration file handling is quite flexible; and it often the case that
+properties may exist that are not bound to a `@ConfigurationProperties` bean. To support
+such cases, the annotation processor will automatically merge items from
+`META-INF/additional-spring-configuration-metadata.json` into the main meta-data file.
+
+The format of the `additional-spring-configuration-metadata.json` file is exactly the same
+as the regular `spring-configuration-metadata.json`. The additional properties file is
+optional, if you don't have any additional properties, simply don't add it.
+
diff --git a/spring-boot-docs/src/main/asciidoc/appendix.adoc b/spring-boot-docs/src/main/asciidoc/appendix.adoc
index 9767f8a6c6..ea8875ca7e 100644
--- a/spring-boot-docs/src/main/asciidoc/appendix.adoc
+++ b/spring-boot-docs/src/main/asciidoc/appendix.adoc
@@ -2,6 +2,7 @@
= Appendices
include::appendix-application-properties.adoc[]
+include::appendix-configuration-metadata.adoc[]
include::appendix-auto-configuration-classes.adoc[]
include::appendix-executable-jar-format.adoc[]
include::appendix-dependency-versions.adoc[]
diff --git a/spring-boot-parent/pom.xml b/spring-boot-parent/pom.xml
index 0275a7c49e..cf48644065 100644
--- a/spring-boot-parent/pom.xml
+++ b/spring-boot-parent/pom.xml
@@ -19,9 +19,10 @@
..
1.6
+ 0.9.1.v20140329
+ 20140107
UTF-8
UTF-8
- 0.9.1.v20140329
20140107
3.1.1
diff --git a/spring-boot-tools/pom.xml b/spring-boot-tools/pom.xml
index a21fbe528e..6dfefa64f2 100644
--- a/spring-boot-tools/pom.xml
+++ b/spring-boot-tools/pom.xml
@@ -20,6 +20,7 @@
${basedir}/..
+ spring-boot-configuration-processor
spring-boot-dependency-tools
spring-boot-loader
spring-boot-loader-tools
diff --git a/spring-boot-tools/spring-boot-configuration-processor/pom.xml b/spring-boot-tools/spring-boot-configuration-processor/pom.xml
new file mode 100644
index 0000000000..71970bbe48
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-tools
+ 1.2.0.BUILD-SNAPSHOT
+
+ spring-boot-configuration-processor
+ Spring Boot Configuration Processor
+ Spring Boot Configuration Processor
+ http://projects.spring.io/spring-boot/
+
+ Pivotal Software, Inc.
+ http://www.spring.io
+
+
+ ${basedir}/../..
+
+
+
+
+ org.json
+ json
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ none
+
+
+
+
+
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java
new file mode 100644
index 0000000000..371bbd2ad1
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java
@@ -0,0 +1,298 @@
+/*
+ * 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.Collections;
+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.Diagnostic.Kind;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+
+import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser;
+import org.springframework.boot.configurationprocessor.fieldvalues.javac.JavaCompilerFieldValuesParser;
+import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
+import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
+import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
+
+/**
+ * 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";
+
+ static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot."
+ + "context.properties.NestedConfigurationProperty";
+
+ private ConfigurationMetadata metadata;
+
+ private TypeUtils typeUtils;
+
+ private FieldValuesParser fieldValuesParser;
+
+ protected String configurationPropertiesAnnotation() {
+ return CONFIGURATION_PROPERTIES_ANNOTATION;
+ }
+
+ protected String nestedConfigurationPropertyAnnotation() {
+ return NESTED_CONFIGURATION_PROPERTY_ANNOTATION;
+ }
+
+ @Override
+ public synchronized void init(ProcessingEnvironment env) {
+ super.init(env);
+ this.metadata = new ConfigurationMetadata();
+ this.typeUtils = new TypeUtils(env);
+ try {
+ this.fieldValuesParser = new JavaCompilerFieldValuesParser(env);
+ }
+ catch (Throwable ex) {
+ this.fieldValuesParser = FieldValuesParser.NONE;
+ logWarning("Field value processing of @ConfigurationProperty meta-data is "
+ + "not supported");
+ }
+ }
+
+ private void logWarning(String msg) {
+ this.processingEnv.getMessager().printMessage(Kind.WARNING, msg);
+ }
+
+ @Override
+ public boolean process(Set extends TypeElement> 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);
+ Map fieldValues = getFieldValues(element);
+ processSimpleTypes(prefix, element, members, fieldValues);
+ processNestedTypes(prefix, element, members);
+ }
+
+ private Map getFieldValues(TypeElement element) {
+ try {
+ return this.fieldValuesParser.getFieldValues(element);
+ }
+ catch (Exception ex) {
+ return Collections.emptyMap();
+ }
+ }
+
+ private void processSimpleTypes(String prefix, TypeElement element,
+ TypeElementMembers members, Map fieldValues) {
+ 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);
+ boolean isNested = getAnnotation(field,
+ nestedConfigurationPropertyAnnotation()) != null;
+ boolean isCollection = this.typeUtils.isCollectionOrMap(getter
+ .getReturnType());
+ if (!isNested && (setter != null || isCollection)) {
+ String dataType = this.typeUtils.getType(getter.getReturnType());
+ String sourceType = this.typeUtils.getType(element);
+ String description = this.typeUtils.getJavaDoc(field);
+ Object defaultValue = fieldValues.get(name);
+ this.metadata.add(ItemMetadata.newProperty(prefix, name, dataType,
+ sourceType, null, description, defaultValue));
+ }
+ }
+ }
+
+ private void processNestedTypes(String prefix, TypeElement element,
+ TypeElementMembers members) {
+ for (Map.Entry entry : members.getPublicGetters()
+ .entrySet()) {
+ String name = entry.getKey();
+ ExecutableElement getter = entry.getValue();
+ VariableElement field = members.getFields().get(name);
+ Element returnType = this.processingEnv.getTypeUtils().asElement(
+ getter.getReturnType());
+ AnnotationMirror annotation = getAnnotation(getter,
+ configurationPropertiesAnnotation());
+ boolean isNested = getAnnotation(field,
+ nestedConfigurationPropertyAnnotation()) != null;
+ if (returnType != null && returnType instanceof TypeElement
+ && annotation == null) {
+ TypeElement returns = (TypeElement) returnType;
+ if (this.typeUtils.isEnclosedIn(returnType, element) || isNested) {
+ String nestedPrefix = ConfigurationMetadata
+ .nestedPrefix(prefix, name);
+ 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) {
+ if (element != null) {
+ 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 extends ExecutableElement, ? extends AnnotationValue> entry : annotation
+ .getElementValues().entrySet()) {
+ values.put(entry.getKey().getSimpleName().toString(), entry.getValue()
+ .getValue());
+ }
+ return values;
+ }
+
+ protected void writeMetaData(ConfigurationMetadata metadata) {
+ metadata = mergeManualMetadata(metadata);
+ try {
+ FileObject resource = this.processingEnv.getFiler().createResource(
+ StandardLocation.CLASS_OUTPUT, "",
+ "META-INF/spring-configuration-metadata.json");
+ OutputStream outputStream = resource.openOutputStream();
+ try {
+ new JsonMarshaller().write(metadata, outputStream);
+ }
+ finally {
+ outputStream.close();
+ }
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private ConfigurationMetadata mergeManualMetadata(ConfigurationMetadata metadata) {
+ try {
+ FileObject manualMetadata = this.processingEnv.getFiler().getResource(
+ StandardLocation.CLASS_PATH, "",
+ "META-INF/additional-spring-configuration-metadata.json");
+ InputStream inputStream = manualMetadata.openInputStream();
+ try {
+ ConfigurationMetadata merged = new ConfigurationMetadata(metadata);
+ try {
+ merged.addAll(new JsonMarshaller().read(inputStream));
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ return merged;
+ }
+ finally {
+ inputStream.close();
+ }
+ }
+ catch (IOException ex) {
+ ex.printStackTrace();
+ return metadata;
+ }
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java
new file mode 100644
index 0000000000..4a5832c3bd
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeElementMembers.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.element.VariableElement;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.util.ElementFilter;
+
+/**
+ * Provides access to relevant {@link TypeElement} members.
+ *
+ * @author Stephane Nicoll
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+class TypeElementMembers {
+
+ private static final String OBJECT_CLASS_NAME = Object.class.getName();
+
+ private final ProcessingEnvironment env;
+
+ private final Map fields = new LinkedHashMap();
+
+ private final Map publicGetters = new LinkedHashMap();
+
+ private final Map publicSetters = new LinkedHashMap();
+
+ public TypeElementMembers(ProcessingEnvironment env, TypeElement element) {
+ this.env = env;
+ process(element);
+ }
+
+ private void process(TypeElement element) {
+ for (ExecutableElement method : ElementFilter.methodsIn(element
+ .getEnclosedElements())) {
+ processMethod(method);
+ }
+ for (VariableElement field : ElementFilter
+ .fieldsIn(element.getEnclosedElements())) {
+ processField(field);
+ }
+ Element superType = this.env.getTypeUtils().asElement(element.getSuperclass());
+ if (superType != null && superType instanceof TypeElement
+ && !OBJECT_CLASS_NAME.equals(superType.toString())) {
+ process((TypeElement) superType);
+ }
+ }
+
+ private void processMethod(ExecutableElement method) {
+ if (method.getModifiers().contains(Modifier.PUBLIC)) {
+ String name = method.getSimpleName().toString();
+ if (isGetter(method) && !this.publicGetters.containsKey(name)) {
+ this.publicGetters.put(getAccessorName(name), method);
+ }
+ else if (isSetter(method) && !this.publicSetters.containsKey(name)) {
+ this.publicSetters.put(getAccessorName(name), method);
+ }
+ }
+ }
+
+ private boolean isGetter(ExecutableElement method) {
+ String name = method.getSimpleName().toString();
+ return (name.startsWith("get") || name.startsWith("is"))
+ && method.getParameters().isEmpty()
+ && (TypeKind.VOID != method.getReturnType().getKind());
+ }
+
+ private boolean isSetter(ExecutableElement method) {
+ final String name = method.getSimpleName().toString();
+ return name.startsWith("set") && method.getParameters().size() == 1
+ && (TypeKind.VOID == method.getReturnType().getKind());
+ }
+
+ private String getAccessorName(String methodName) {
+ String name = methodName.startsWith("is") ? methodName.substring(2) : methodName
+ .substring(3);
+ name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
+ return name;
+ }
+
+ private void processField(VariableElement field) {
+ String name = field.getSimpleName().toString();
+ if (!this.fields.containsKey(name)) {
+ this.fields.put(name, field);
+ }
+ }
+
+ public Map getFields() {
+ return Collections.unmodifiableMap(this.fields);
+ }
+
+ public Map getPublicGetters() {
+ return Collections.unmodifiableMap(this.publicGetters);
+ }
+
+ public Map getPublicSetters() {
+ return Collections.unmodifiableMap(this.publicSetters);
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java
new file mode 100644
index 0000000000..10789f8665
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/TypeUtils.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.WildcardType;
+import javax.lang.model.util.Types;
+
+/**
+ * Type Utilities.
+ *
+ * @author Stephane Nicoll
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+class TypeUtils {
+
+ private static final Map> PRIMITIVE_WRAPPERS;
+ static {
+ Map> wrappers = new HashMap>();
+ wrappers.put(TypeKind.BOOLEAN, Boolean.class);
+ wrappers.put(TypeKind.BYTE, Byte.class);
+ wrappers.put(TypeKind.CHAR, Character.class);
+ wrappers.put(TypeKind.DOUBLE, Double.class);
+ wrappers.put(TypeKind.FLOAT, Float.class);
+ wrappers.put(TypeKind.INT, Integer.class);
+ wrappers.put(TypeKind.LONG, Long.class);
+ wrappers.put(TypeKind.SHORT, Short.class);
+ PRIMITIVE_WRAPPERS = Collections.unmodifiableMap(wrappers);
+ }
+
+ private final ProcessingEnvironment env;
+
+ private final TypeMirror collectionType;
+
+ private final TypeMirror mapType;
+
+ public TypeUtils(ProcessingEnvironment env) {
+ this.env = env;
+ Types types = env.getTypeUtils();
+ WildcardType wc = types.getWildcardType(null, null);
+ this.collectionType = types.getDeclaredType(this.env.getElementUtils()
+ .getTypeElement(Collection.class.getName()), wc);
+ this.mapType = types.getDeclaredType(
+ this.env.getElementUtils().getTypeElement(Map.class.getName()), wc, wc);
+
+ }
+
+ public String getType(Element element) {
+ return getType(element == null ? null : element.asType());
+ }
+
+ public String getType(TypeMirror type) {
+ if (type == null) {
+ return null;
+ }
+ Class> wrapper = PRIMITIVE_WRAPPERS.get(type.getKind());
+ if (wrapper != null) {
+ return wrapper.getName();
+ }
+ if (type instanceof DeclaredType) {
+ DeclaredType declaredType = (DeclaredType) type;
+ Element enclosingElement = declaredType.asElement().getEnclosingElement();
+ if (enclosingElement != null && enclosingElement instanceof TypeElement) {
+ return getType(enclosingElement) + "$"
+ + declaredType.asElement().getSimpleName().toString();
+ }
+ }
+ return type.toString();
+ }
+
+ public boolean isCollectionOrMap(TypeMirror type) {
+ return this.env.getTypeUtils().isAssignable(type, this.collectionType)
+ || this.env.getTypeUtils().isAssignable(type, this.mapType);
+ }
+
+ public boolean isEnclosedIn(Element candidate, TypeElement element) {
+ if (candidate == null || element == null) {
+ return false;
+ }
+ if (candidate.equals(element)) {
+ return true;
+ }
+ return isEnclosedIn(candidate.getEnclosingElement(), element);
+ }
+
+ public String getJavaDoc(Element element) {
+ String javadoc = (element == null ? null : this.env.getElementUtils()
+ .getDocComment(element));
+ if (javadoc != null) {
+ javadoc = javadoc.trim();
+ }
+ return ("".equals(javadoc) ? null : javadoc);
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/FieldValuesParser.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/FieldValuesParser.java
new file mode 100644
index 0000000000..e2e73d3a7f
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/FieldValuesParser.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor.fieldvalues;
+
+import java.util.Collections;
+import java.util.Map;
+
+import javax.lang.model.element.TypeElement;
+
+import org.springframework.boot.configurationprocessor.fieldvalues.javac.JavaCompilerFieldValuesParser;
+
+/**
+ * Parser which can be used to obtain the field values from an {@link TypeElement}.
+ *
+ * @author Phillip Webb
+ * @since 1.1.2
+ * @see JavaCompilerFieldValuesParser
+ */
+public interface FieldValuesParser {
+
+ /**
+ * Implementation of {@link FieldValuesParser} that always returns an empty
+ * result.
+ */
+ public static final FieldValuesParser NONE = new FieldValuesParser() {
+
+ @Override
+ public Map getFieldValues(TypeElement element) {
+ return Collections.emptyMap();
+ }
+
+ };
+
+ /**
+ * Return the field values for the given element.
+ * @param element the element to inspect
+ * @return a map of field names to values.
+ * @throws Exception if the values cannot be extracted
+ */
+ Map getFieldValues(TypeElement element) throws Exception;
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java
new file mode 100644
index 0000000000..6ad9cc77f2
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ExpressionTree.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor.fieldvalues.javac;
+
+import java.lang.reflect.Method;
+
+/**
+ * Reflection based access to {@code com.sun.source.tree.ExpressionTree}.
+ *
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+class ExpressionTree extends ReflectionWrapper {
+
+ private final Class> literalTreeType = findClass("com.sun.source.tree.LiteralTree");
+
+ private final Method literalValueMethod = findMethod(this.literalTreeType, "getValue");
+
+ public ExpressionTree(Object instance) {
+ super(instance);
+ }
+
+ public String getKind() throws Exception {
+ return findMethod("getKind").invoke(getInstance()).toString();
+ }
+
+ public Object getLiteralValue() throws Exception {
+ if (this.literalTreeType.isAssignableFrom(getInstance().getClass())) {
+ return this.literalValueMethod.invoke(getInstance());
+ }
+ return null;
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java
new file mode 100644
index 0000000000..8f7e28b1df
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/JavaCompilerFieldValuesParser.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor.fieldvalues.javac;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.element.TypeElement;
+
+import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser;
+
+/**
+ * {@link FieldValuesParser} implementation for the standard Java compiler.
+ *
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+public class JavaCompilerFieldValuesParser implements FieldValuesParser {
+
+ private final Trees trees;
+
+ public JavaCompilerFieldValuesParser(ProcessingEnvironment env) throws Exception {
+ this.trees = Trees.instance(env);
+ }
+
+ @Override
+ public Map getFieldValues(TypeElement element) throws Exception {
+ Tree tree = this.trees.getTree(element);
+ if (tree != null) {
+ FieldCollector fieldCollector = new FieldCollector();
+ tree.accept(fieldCollector);
+ return fieldCollector.getFieldValues();
+ }
+ return Collections.emptyMap();
+ }
+
+ private static class FieldCollector implements TreeVisitor {
+
+ private static final Map> WRAPPER_TYPES;
+ static {
+ Map> types = new HashMap>();
+ types.put("boolean", Boolean.class);
+ types.put(Boolean.class.getName(), Boolean.class);
+ types.put("byte", Byte.class);
+ types.put(Byte.class.getName(), Byte.class);
+ types.put("short", Short.class);
+ types.put(Short.class.getName(), Short.class);
+ types.put("int", Integer.class);
+ types.put(Integer.class.getName(), Integer.class);
+ types.put("long", Long.class);
+ types.put(Long.class.getName(), Long.class);
+ WRAPPER_TYPES = Collections.unmodifiableMap(types);
+ }
+
+ private static final Map, Object> DEFAULT_TYPE_VALUES;
+ static {
+ Map, Object> values = new HashMap, Object>();
+ values.put(Boolean.class, false);
+ values.put(Byte.class, (byte) 0);
+ values.put(Short.class, (short) 0);
+ values.put(Integer.class, 0);
+ values.put(Long.class, (long) 0);
+ DEFAULT_TYPE_VALUES = Collections.unmodifiableMap(values);
+ }
+
+ private static final Map WELL_KNOWN_STATIC_FINALS;
+ static {
+ Map values = new HashMap();
+ values.put("Boolean.TRUE", true);
+ values.put("Boolean.FALSE", false);
+ WELL_KNOWN_STATIC_FINALS = Collections.unmodifiableMap(values);
+ }
+
+ private final Map fieldValues = new HashMap();
+
+ private final Map staticFinals = new HashMap();
+
+ @Override
+ public void visitVariable(VariableTree variable) throws Exception {
+ Set flags = variable.getModifierFlags();
+ if (flags.contains(Modifier.STATIC) && flags.contains(Modifier.FINAL)) {
+ this.staticFinals.put(variable.getName(), getValue(variable));
+ }
+ if (!flags.contains(Modifier.FINAL)) {
+ this.fieldValues.put(variable.getName(), getValue(variable));
+ }
+ }
+
+ private Object getValue(VariableTree variable) throws Exception {
+ ExpressionTree initializer = variable.getInitializer();
+ Class> wrapperType = WRAPPER_TYPES.get(variable.getType());
+ if (initializer != null) {
+ if (initializer.getLiteralValue() != null) {
+ return initializer.getLiteralValue();
+ }
+ if (initializer.getKind().equals("IDENTIFIER")) {
+ return this.staticFinals.get(initializer.toString());
+ }
+ if (initializer.getKind().equals("MEMBER_SELECT")) {
+ return WELL_KNOWN_STATIC_FINALS.get(initializer.toString());
+ }
+ }
+ return DEFAULT_TYPE_VALUES.get(wrapperType);
+ }
+
+ public Map getFieldValues() {
+ return this.fieldValues;
+ }
+
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java
new file mode 100644
index 0000000000..f4f3b19101
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/ReflectionWrapper.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor.fieldvalues.javac;
+
+import java.lang.reflect.Method;
+
+/**
+ * Base class for reflection based wrappers. Used to access internal Java classes without
+ * needing tools.jar on the classpath.
+ *
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+class ReflectionWrapper {
+
+ private final Class> type;
+
+ private final Object instance;
+
+ public ReflectionWrapper(Object instance) {
+ this.type = instance.getClass();
+ this.instance = instance;
+ }
+
+ public ReflectionWrapper(String type, Object instance) {
+ this.type = findClass(instance.getClass().getClassLoader(), type);
+ this.instance = instance;
+ }
+
+ protected final Object getInstance() {
+ return this.instance;
+ }
+
+ @Override
+ public String toString() {
+ return this.instance.toString();
+ }
+
+ protected Class> findClass(String name) {
+ return findClass(getInstance().getClass().getClassLoader(), name);
+ }
+
+ protected Method findMethod(String name, Class>... parameterTypes) {
+ return findMethod(this.type, name, parameterTypes);
+ }
+
+ protected static Class> findClass(ClassLoader classLoader, String name) {
+ try {
+ return classLoader.loadClass(name);
+ }
+ catch (ClassNotFoundException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ protected static Method findMethod(Class> type, String name,
+ Class>... parameterTypes) {
+ try {
+ return type.getMethod(name, parameterTypes);
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Tree.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Tree.java
new file mode 100644
index 0000000000..86c197f762
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Tree.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor.fieldvalues.javac;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+/**
+ * Reflection based access to {@code com.sun.source.tree.Tree}.
+ *
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+class Tree extends ReflectionWrapper {
+
+ private final Class> treeVisitorType = findClass("com.sun.source.tree.TreeVisitor");
+
+ private final Method acceptMethod = findMethod("accept", this.treeVisitorType,
+ Object.class);
+
+ private final Method GET_CLASS_TREE_MEMBERS = findMethod(
+ findClass("com.sun.source.tree.ClassTree"), "getMembers");
+
+ public Tree(Object instance) {
+ super("com.sun.source.tree.Tree", instance);
+ }
+
+ public void accept(TreeVisitor visitor) throws Exception {
+ this.acceptMethod.invoke(getInstance(), Proxy.newProxyInstance(getInstance()
+ .getClass().getClassLoader(), new Class>[] { this.treeVisitorType },
+ new TreeVisitorInvocationHandler(visitor)), 0);
+ }
+
+ /**
+ * {@link InvocationHandler} to call the {@link TreeVisitor}.
+ */
+ private class TreeVisitorInvocationHandler implements InvocationHandler {
+
+ private TreeVisitor treeVisitor;
+
+ public TreeVisitorInvocationHandler(TreeVisitor treeVisitor) {
+ this.treeVisitor = treeVisitor;
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ if (method.getName().equals("visitClass")) {
+ if ((Integer) args[1] == 0) {
+ Iterable members = (Iterable) Tree.this.GET_CLASS_TREE_MEMBERS
+ .invoke(args[0]);
+ for (Object member : members) {
+ if (member != null) {
+ Tree.this.acceptMethod.invoke(member, proxy,
+ ((Integer) args[1]) + 1);
+ }
+ }
+ }
+ }
+ if (method.getName().equals("visitVariable")) {
+ this.treeVisitor.visitVariable(new VariableTree(args[0]));
+ }
+ return null;
+ }
+
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/TreeVisitor.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/TreeVisitor.java
new file mode 100644
index 0000000000..b73cbc28d5
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/TreeVisitor.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor.fieldvalues.javac;
+
+/**
+ * Reflection base alternative for {@code com.sun.source.tree.TreeVisitor}.
+ *
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+interface TreeVisitor {
+
+ void visitVariable(VariableTree variable) throws Exception;
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java
new file mode 100644
index 0000000000..639ecfc184
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/Trees.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor.fieldvalues.javac;
+
+import java.lang.reflect.Method;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.Element;
+
+/**
+ * Reflection based access to {@code com.sun.source.util.Trees}.
+ *
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+class Trees extends ReflectionWrapper {
+
+ private Trees(Object instance) {
+ super(instance);
+ }
+
+ public Tree getTree(Element element) throws Exception {
+ Object tree = findMethod("getTree", Element.class).invoke(getInstance(), element);
+ return (tree == null ? null : new Tree(tree));
+ }
+
+ public static Trees instance(ProcessingEnvironment env) throws Exception {
+ ClassLoader classLoader = env.getClass().getClassLoader();
+ Class> type = findClass(classLoader, "com.sun.source.util.Trees");
+ Method method = findMethod(type, "instance", ProcessingEnvironment.class);
+ return new Trees(method.invoke(null, env));
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/VariableTree.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/VariableTree.java
new file mode 100644
index 0000000000..4817a4a78e
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/fieldvalues/javac/VariableTree.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor.fieldvalues.javac;
+
+import java.util.Collections;
+import java.util.Set;
+
+import javax.lang.model.element.Modifier;
+
+/**
+ * Reflection based access to {@code com.sun.source.tree.VariableTree}.
+ *
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+class VariableTree extends ReflectionWrapper {
+
+ public VariableTree(Object instance) {
+ super(instance);
+ }
+
+ public String getName() throws Exception {
+ return findMethod("getName").invoke(getInstance()).toString();
+ }
+
+ public String getType() throws Exception {
+ return findMethod("getType").invoke(getInstance()).toString();
+ }
+
+ public ExpressionTree getInitializer() throws Exception {
+ Object instance = findMethod("getInitializer").invoke(getInstance());
+ return (instance == null ? null : new ExpressionTree(instance));
+ }
+
+ @SuppressWarnings("unchecked")
+ public Set getModifierFlags() throws Exception {
+ Object modifiers = findMethod("getModifiers").invoke(getInstance());
+ if (modifiers == null) {
+ return Collections.emptySet();
+ }
+ return (Set) findMethod(findClass("com.sun.source.tree.ModifiersTree"),
+ "getFlags").invoke(modifiers);
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java
new file mode 100644
index 0000000000..8d44ffe180
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor.metadata;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Configuration meta-data.
+ *
+ * @author Stephane Nicoll
+ * @author Phillip Webb
+ * @since 1.2.0
+ * @see ItemMetadata
+ */
+public class ConfigurationMetadata {
+
+ private final List items;
+
+ public ConfigurationMetadata() {
+ this.items = new ArrayList();
+ }
+
+ public ConfigurationMetadata(ConfigurationMetadata metadata) {
+ this.items = new ArrayList(metadata.getItems());
+ }
+
+ /**
+ * Add item meta-data.
+ * @param itemMetadata the meta-data to add
+ */
+ public void add(ItemMetadata itemMetadata) {
+ this.items.add(itemMetadata);
+ Collections.sort(this.items);
+ }
+
+ /**
+ * Add all properties from another {@link ConfigurationMetadata}.
+ * @param metadata the {@link ConfigurationMetadata} instance to merge
+ */
+ public void addAll(ConfigurationMetadata metadata) {
+ this.items.addAll(metadata.getItems());
+ Collections.sort(this.items);
+ }
+
+ /**
+ * @return the meta-data properties.
+ */
+ public List getItems() {
+ return Collections.unmodifiableList(this.items);
+ }
+
+ public static String nestedPrefix(String prefix, String name) {
+ String nestedPrefix = (prefix == null ? "" : prefix);
+ String dashedName = toDashedCase(name);
+ nestedPrefix += ("".equals(nestedPrefix) ? dashedName : "." + dashedName);
+ return nestedPrefix;
+ }
+
+ static String toDashedCase(String name) {
+ StringBuilder dashed = new StringBuilder();
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ if (Character.isUpperCase(c) && dashed.length() > 0) {
+ dashed.append("-");
+ }
+ dashed.append(Character.toLowerCase(c));
+ }
+ return dashed.toString();
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java
new file mode 100644
index 0000000000..f261f4a9b5
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemMetadata.java
@@ -0,0 +1,138 @@
+/*
+ * 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;
+
+ private Object defaultValue;
+
+ ItemMetadata(ItemType itemType, String prefix, String name, String type,
+ String sourceType, String sourceMethod, String description,
+ Object defaultValue) {
+ super();
+ this.itemType = itemType;
+ this.name = buildName(prefix, name);
+ this.type = type;
+ this.sourceType = sourceType;
+ this.sourceMethod = sourceMethod;
+ this.description = description;
+ this.defaultValue = defaultValue;
+ }
+
+ 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;
+ }
+
+ public Object getDefaultValue() {
+ return this.defaultValue;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder string = new StringBuilder(this.name);
+ buildToStringProperty(string, "type", this.type);
+ buildToStringProperty(string, "sourceType", this.sourceType);
+ buildToStringProperty(string, "description", this.description);
+ buildToStringProperty(string, "defaultValue", this.defaultValue);
+ return string.toString();
+ }
+
+ 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, null);
+ }
+
+ public static ItemMetadata newProperty(String prefix, String name, String type,
+ String sourceType, String sourceMethod, String description,
+ Object defaultValue) {
+ return new ItemMetadata(ItemType.PROPERTY, prefix, name, type, sourceType,
+ sourceMethod, description, defaultValue);
+ }
+
+ /**
+ * The item type.
+ */
+ public static enum ItemType {
+ GROUP, PROPERTY
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java
new file mode 100644
index 0000000000..c05f7b6156
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java
@@ -0,0 +1,142 @@
+/*
+ * 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());
+ putIfPresent(jsonObject, "defaultValue", item.getDefaultValue());
+ 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);
+ Object defaultValue = object.opt("defaultValue");
+ return new ItemMetadata(itemType, name, null, type, sourceType, sourceMethod,
+ description, defaultValue);
+ }
+
+ private String toString(InputStream inputStream) throws IOException {
+ StringBuilder out = new StringBuilder();
+ InputStreamReader reader = new InputStreamReader(inputStream, UTF_8);
+ char[] buffer = new char[BUFFER_SIZE];
+ int bytesRead;
+ while ((bytesRead = reader.read(buffer)) != -1) {
+ out.append(buffer, 0, bytesRead);
+ }
+ return out.toString();
+ }
+
+ /**
+ * Extension to {@link JSONObject} that remembers the order of inserts.
+ */
+ @SuppressWarnings("rawtypes")
+ private static class JSONOrderedObject extends JSONObject {
+
+ private Set keys = new LinkedHashSet();
+
+ @Override
+ public JSONObject put(String key, Object value) throws JSONException {
+ this.keys.add(key);
+ return super.put(key, value);
+ }
+
+ @Override
+ public Set keySet() {
+ return this.keys;
+ }
+
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/spring-boot-tools/spring-boot-configuration-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 0000000000..e01209af7c
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+org.springframework.boot.configurationprocessor.ConfigurationMetadataAnnotationProcessor
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java
new file mode 100644
index 0000000000..e4d8ffb0de
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java
@@ -0,0 +1,293 @@
+/*
+ * 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 org.springframework.boot.configurationsample.specific.SimplePojo;
+
+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.")
+ .withDefaultValue("boot"));
+ 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(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";
+
+ static final String NESTED_CONFIGURATION_PROPERTY_ANNOTATION = "org.springframework.boot.configurationsample.NestedConfigurationProperty";
+
+ private ConfigurationMetadata metadata;
+
+ @Override
+ protected String configurationPropertiesAnnotation() {
+ return CONFIGURATION_PROPERTIES_ANNOTATION;
+ }
+
+ @Override
+ protected String nestedConfigurationPropertyAnnotation() {
+ return NESTED_CONFIGURATION_PROPERTY_ANNOTATION;
+ }
+
+ @Override
+ protected void writeMetaData(ConfigurationMetadata metadata) {
+ this.metadata = metadata;
+ }
+
+ public ConfigurationMetadata getMetadata() {
+ return this.metadata;
+ }
+
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java
new file mode 100644
index 0000000000..0ffe042db4
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java
@@ -0,0 +1,176 @@
+/*
+ * 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;
+
+ private final Object defaultValue;
+
+ public ContainsItemMatcher(ItemType itemType, String name) {
+ this(itemType, name, null, null, null, null);
+ }
+
+ public ContainsItemMatcher(ItemType itemType, String name, String type,
+ Class> sourceType, String description, Object defaultValue) {
+ this.itemType = itemType;
+ this.name = name;
+ this.type = type;
+ this.sourceType = sourceType;
+ this.description = description;
+ this.defaultValue = defaultValue;
+ }
+
+ @Override
+ 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.defaultValue != null
+ && !this.defaultValue.equals(itemMetadata.getDefaultValue())) {
+ 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.defaultValue != null) {
+ description.appendText(" defaultValue ").appendValue(this.defaultValue);
+ }
+ 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, this.defaultValue);
+ }
+
+ public ContainsItemMatcher ofDataType(String dataType) {
+ return new ContainsItemMatcher(this.itemType, this.name, dataType,
+ this.sourceType, this.description, this.defaultValue);
+ }
+
+ public ContainsItemMatcher fromSource(Class> sourceType) {
+ return new ContainsItemMatcher(this.itemType, this.name, this.type,
+ sourceType, this.description, this.defaultValue);
+ }
+
+ public ContainsItemMatcher withDescription(String description) {
+ return new ContainsItemMatcher(this.itemType, this.name, this.type,
+ this.sourceType, description, this.defaultValue);
+ }
+
+ public Matcher super ConfigurationMetadata> withDefaultValue(Object defaultValue) {
+ return new ContainsItemMatcher(this.itemType, this.name, this.type,
+ this.sourceType, this.description, defaultValue);
+ }
+
+ private ItemMetadata getFirstPropertyWithName(ConfigurationMetadata metadata,
+ String name) {
+ for (ItemMetadata item : metadata.getItems()) {
+ if (item.isOfItemType(this.itemType) && name.equals(item.getName())) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ }
+
+}
diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestCompiler.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestCompiler.java
new file mode 100644
index 0000000000..b10d0ece5d
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestCompiler.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.annotation.processing.Processor;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Wrapper to make the {@link JavaCompiler} easier to use in tests.
+ *
+ * @author Stephane Nicoll
+ * @author Phillip Webb
+ */
+public class TestCompiler {
+
+ private final JavaCompiler compiler;
+
+ private final StandardJavaFileManager fileManager;
+
+ public TestCompiler(TemporaryFolder temporaryFolder) throws IOException {
+ this(ToolProvider.getSystemJavaCompiler(), temporaryFolder);
+ }
+
+ public TestCompiler(JavaCompiler compiler, TemporaryFolder temporaryFolder)
+ throws IOException {
+ this.compiler = compiler;
+ this.fileManager = compiler.getStandardFileManager(null, null, null);
+ Iterable extends File> 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 extends JavaFileObject> javaFileObjects = getJavaFileObjects(types);
+ return new TestCompilationTask(this.compiler.getTask(null, this.fileManager,
+ null, null, null, javaFileObjects));
+ }
+
+ private Iterable extends JavaFileObject> 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/fieldvalues/AbstractFieldValuesProcessorTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java
new file mode 100644
index 0000000000..7cd2fa2e1e
--- /dev/null
+++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/fieldvalues/AbstractFieldValuesProcessorTests.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.configurationprocessor.fieldvalues;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.annotation.processing.SupportedSourceVersion;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+
+import org.hamcrest.Matcher;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.springframework.boot.configurationprocessor.TestCompiler;
+import org.springframework.boot.configurationsample.fieldvalues.FieldValues;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Abstract base class for {@link FieldValuesParser} tests.
+ *
+ * @author Phillip Webb
+ */
+public abstract class AbstractFieldValuesProcessorTests {
+
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ protected abstract FieldValuesParser createProcessor(ProcessingEnvironment env);
+
+ @Test
+ public void getFieldValues() throws Exception {
+ TestProcessor processor = new TestProcessor();
+ TestCompiler compiler = new TestCompiler(this.temporaryFolder);
+ compiler.getTask(FieldValues.class).call(processor);
+ Map values = processor.getValues();
+ assertThat(values.get("string"), equalToObject("1"));
+ assertThat(values.get("stringNone"), nullValue());
+ assertThat(values.get("stringConst"), equalToObject("c"));
+ assertThat(values.get("bool"), equalToObject(true));
+ assertThat(values.get("boolNone"), equalToObject(false));
+ assertThat(values.get("boolConst"), equalToObject(true));
+ assertThat(values.get("boolObject"), equalToObject(true));
+ assertThat(values.get("boolObjectNone"), nullValue());
+ assertThat(values.get("boolObjectConst"), equalToObject(true));
+ assertThat(values.get("integer"), equalToObject(1));
+ assertThat(values.get("integerNone"), equalToObject(0));
+ assertThat(values.get("integerConst"), equalToObject(2));
+ assertThat(values.get("integerObject"), equalToObject(3));
+ assertThat(values.get("integerObjectNone"), nullValue());
+ assertThat(values.get("integerObjectConst"), equalToObject(4));
+ assertThat(values.get("object"), equalToObject(123));
+ assertThat(values.get("objectNone"), nullValue());
+ assertThat(values.get("objectConst"), equalToObject("c"));
+ assertThat(values.get("objectInstance"), nullValue());
+ }
+
+ private Matcher
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
org.apache.httpcomponents
diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java
new file mode 100644
index 0000000000..ebb78b3491
--- /dev/null
+++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/NestedConfigurationProperty.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2012-2014 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.boot.context.properties;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Indicates that a field in a {@link ConfigurationProperties} object should be treated as
+ * if it were a nested type. This annotation has no bearing on the actual binding
+ * processes, but it is used by the {@code spring-boot-configuration-processor} as a hint
+ * that a field is not bound as a single value.
+ *
+ * @author Stephane Nicoll
+ * @author Phillip Webb
+ * @since 1.2.0
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface NestedConfigurationProperty {
+
+}
diff --git a/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json
new file mode 100644
index 0000000000..526c4af3fa
--- /dev/null
+++ b/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json
@@ -0,0 +1,73 @@
+{"groups": [
+ {
+ "name": "logging",
+ "sourceType": "org.springframework.boot.logging.LoggingApplicationListener"
+ }
+],"properties": [
+ {
+ "name": "logging.config",
+ "dataType": "java.lang.String",
+ "description": "Location of the logging configuration file."
+ },
+ {
+ "name": "logging.file",
+ "dataType": "java.lang.String",
+ "description": "The name of the log file."
+ },
+ {
+ "name": "logging.path",
+ "dataType": "java.lang.String",
+ "description": "Location of the log file."
+ },
+ {
+ "name": "spring.mandatory-file-encoding",
+ "sourceType": "org.springframework.boot.context.FileEncodingApplicationListener",
+ "dataType": "java.lang.String",
+ "description": "The character encoding the application must use."
+ },
+ {
+ "name": "spring.application.name",
+ "dataType": "java.lang.String",
+ "sourceType": "org.springframework.boot.context.ContextIdApplicationContextInitializer",
+ "description": "The name of the application."
+ },
+ {
+ "name": "spring.application.index",
+ "dataType": "java.lang.Integer",
+ "sourceType": "org.springframework.boot.context.ContextIdApplicationContextInitializer",
+ "description": "Index of the application."
+ },
+ {
+ "name": "spring.config.name",
+ "dataType": "java.lang.String",
+ "sourceType": "org.springframework.boot.context.config.ConfigFileApplicationListener",
+ "description": "Config file name",
+ "defaultValue": "application",
+ },
+ {
+ "name": "spring.config.location",
+ "dataType": "java.lang.String",
+ "sourceType": "org.springframework.boot.context.config.ConfigFileApplicationListener",
+ "description": "Config file locations",
+ },
+ {
+ "name": "spring.main.show-banner",
+ "dataType": "java.lang.Boolean",
+ "sourceType": "org.springframework.boot.SpringApplication",
+ "description": "Display the banner when the application runs",
+ "defaultValue": true,
+ },
+ {
+ "name": "spring.main.sources",
+ "dataType": "java.util.Set",
+ "sourceType": "org.springframework.boot.SpringApplication",
+ "description": "Sources (class name, package name or XML resource location) used to create the ApplicationContext."
+ },
+ {
+ "name": "spring.main.web-environment",
+ "dataType": "java.lang.Boolean",
+ "sourceType": "org.springframework.boot.SpringApplication",
+ "description": "Run the application in a web environment (auto-detected by default)"
+ }
+]}
+