Merge branch 'gh-2313'

pull/2400/merge
Andy Wilkinson 10 years ago
commit afe3bc0938

@ -29,6 +29,10 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
@ -36,7 +40,7 @@
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<configuration> <configuration>
<!-- Ensure own annotation processor doens't kick in --> <!-- Ensure own annotation processor doesn't kick in -->
<proc>none</proc> <proc>none</proc>
</configuration> </configuration>
</plugin> </plugin>

@ -0,0 +1,42 @@
/*
* Copyright 2012-2015 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 javax.annotation.processing.RoundEnvironment;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
/**
* A {@code BuildTracker} tracks a build in which configuration processing has been
* performed and is responsible for managing the associated state including the resulting
* metadata.
*
* @author Andy Wilkinson
*/
public interface BuildHandler {
void addGroup(String name, String type, String sourceType, String sourceMethod);
void addProperty(String prefix, String name, String type, String sourceType,
String sourceMethod, String description, Object defaultValue,
boolean deprecated);
void processing(RoundEnvironment environment);
ConfigurationMetadata produceMetadata();
}

@ -16,12 +16,8 @@
package org.springframework.boot.configurationprocessor; package org.springframework.boot.configurationprocessor;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.util.Collections; import java.util.Collections;
@ -47,14 +43,10 @@ import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements; import javax.lang.model.util.Elements;
import javax.tools.Diagnostic.Kind; 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.FieldValuesParser;
import org.springframework.boot.configurationprocessor.fieldvalues.javac.JavaCompilerFieldValuesParser; import org.springframework.boot.configurationprocessor.fieldvalues.javac.JavaCompilerFieldValuesParser;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; 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 * Annotation {@link Processor} that writes meta-data file for
@ -62,6 +54,7 @@ import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Phillip Webb * @author Phillip Webb
* @author Kris De Volder
* @since 1.2.0 * @since 1.2.0
*/ */
@SupportedAnnotationTypes({ "*" }) @SupportedAnnotationTypes({ "*" })
@ -79,11 +72,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
static final String LOMBOK_SETTER_ANNOTATION = "lombok.Setter"; static final String LOMBOK_SETTER_ANNOTATION = "lombok.Setter";
private static final String RESOURCES_FOLDER = "resources"; private MetadataStore metadataStore;
private static final String CLASSES_FOLDER = "classes"; private BuildHandler buildHandler;
private ConfigurationMetadata metadata;
private TypeUtils typeUtils; private TypeUtils typeUtils;
@ -107,8 +98,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
@Override @Override
public synchronized void init(ProcessingEnvironment env) { public synchronized void init(ProcessingEnvironment env) {
super.init(env); super.init(env);
this.metadata = new ConfigurationMetadata();
this.typeUtils = new TypeUtils(env); this.typeUtils = new TypeUtils(env);
this.metadataStore = new MetadataStore(env);
this.buildHandler = createBuildHandler(env, this.metadataStore);
try { try {
this.fieldValuesParser = new JavaCompilerFieldValuesParser(env); this.fieldValuesParser = new JavaCompilerFieldValuesParser(env);
} }
@ -119,16 +111,28 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
} }
} }
private BuildHandler createBuildHandler(ProcessingEnvironment env,
MetadataStore metadataStore) {
ConfigurationMetadata existingMetadata = metadataStore.readMetadata();
if (existingMetadata != null) {
return new IncrementalBuildHandler(env, existingMetadata);
}
else {
return new StandardBuildHandler();
}
}
@Override @Override
public boolean process(Set<? extends TypeElement> annotations, public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) { RoundEnvironment roundEnv) {
this.buildHandler.processing(roundEnv);
Elements elementUtils = this.processingEnv.getElementUtils(); Elements elementUtils = this.processingEnv.getElementUtils();
for (Element element : roundEnv.getElementsAnnotatedWith(elementUtils for (Element element : roundEnv.getElementsAnnotatedWith(elementUtils
.getTypeElement(configurationPropertiesAnnotation()))) { .getTypeElement(configurationPropertiesAnnotation()))) {
processElement(element); processElement(element);
} }
if (roundEnv.processingOver()) { if (roundEnv.processingOver()) {
writeMetaData(this.metadata); writeMetaData();
} }
return false; return false;
} }
@ -149,7 +153,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
private void processAnnotatedTypeElement(String prefix, TypeElement element) { private void processAnnotatedTypeElement(String prefix, TypeElement element) {
String type = this.typeUtils.getType(element); String type = this.typeUtils.getType(element);
this.metadata.add(ItemMetadata.newGroup(prefix, type, type, null)); this.buildHandler.addGroup(prefix, type, type, null);
processTypeElement(prefix, element); processTypeElement(prefix, element);
} }
@ -159,10 +163,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
Element returns = this.processingEnv.getTypeUtils().asElement( Element returns = this.processingEnv.getTypeUtils().asElement(
element.getReturnType()); element.getReturnType());
if (returns instanceof TypeElement) { if (returns instanceof TypeElement) {
this.metadata.add(ItemMetadata.newGroup(prefix, this.buildHandler.addGroup(prefix, this.typeUtils.getType(returns),
this.typeUtils.getType(returns),
this.typeUtils.getType(element.getEnclosingElement()), this.typeUtils.getType(element.getEnclosingElement()),
element.toString())); element.toString());
processTypeElement(prefix, (TypeElement) returns); processTypeElement(prefix, (TypeElement) returns);
} }
} }
@ -207,8 +210,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
boolean deprecated = hasDeprecateAnnotation(getter) boolean deprecated = hasDeprecateAnnotation(getter)
|| hasDeprecateAnnotation(setter) || hasDeprecateAnnotation(setter)
|| hasDeprecateAnnotation(element); || hasDeprecateAnnotation(element);
this.metadata.add(ItemMetadata.newProperty(prefix, name, dataType, this.buildHandler.addProperty(prefix, name, dataType, sourceType, null,
sourceType, null, description, defaultValue, deprecated)); description, defaultValue, deprecated);
} }
} }
} }
@ -235,8 +238,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
Object defaultValue = fieldValues.get(name); Object defaultValue = fieldValues.get(name);
boolean deprecated = hasDeprecateAnnotation(field) boolean deprecated = hasDeprecateAnnotation(field)
|| hasDeprecateAnnotation(element); || hasDeprecateAnnotation(element);
this.metadata.add(ItemMetadata.newProperty(prefix, name, dataType, this.buildHandler.addProperty(prefix, name, dataType, sourceType, null,
sourceType, null, description, defaultValue, deprecated)); description, defaultValue, deprecated);
} }
} }
} }
@ -269,9 +272,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
if (returnType != null && returnType instanceof TypeElement if (returnType != null && returnType instanceof TypeElement
&& annotation == null && isNested) { && annotation == null && isNested) {
String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, name); String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, name);
this.metadata.add(ItemMetadata.newGroup(nestedPrefix, this.buildHandler.addGroup(nestedPrefix,
this.typeUtils.getType(returnType), this.typeUtils.getType(returnType),
this.typeUtils.getType(element), getter.toString())); this.typeUtils.getType(element), getter.toString());
processTypeElement(nestedPrefix, (TypeElement) returnType); processTypeElement(nestedPrefix, (TypeElement) returnType);
} }
} }
@ -328,50 +331,33 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
return values; return values;
} }
protected void writeMetaData(ConfigurationMetadata metadata) { protected ConfigurationMetadata writeMetaData() {
ConfigurationMetadata metadata = this.buildHandler.produceMetadata();
metadata = mergeAdditionalMetadata(metadata); metadata = mergeAdditionalMetadata(metadata);
if (!metadata.getItems().isEmpty()) { if (!metadata.getItems().isEmpty()) {
try { try {
FileObject resource = this.processingEnv.getFiler().createResource( this.metadataStore.writeMetadata(metadata);
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) { catch (IOException ex) {
throw new IllegalStateException(ex); throw new IllegalStateException("Failed to write metadata", ex);
} }
return metadata;
} }
return null;
} }
private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata metadata) { private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata metadata) {
try { try {
InputStream inputStream = getAdditionalMetadata(); ConfigurationMetadata merged = new ConfigurationMetadata(metadata);
try { merged.addAll(this.metadataStore.readAdditionalMetadata());
ConfigurationMetadata merged = new ConfigurationMetadata(metadata); return merged;
try {
merged.addAll(new JsonMarshaller().read(inputStream));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return merged;
}
finally {
inputStream.close();
}
} }
catch (FileNotFoundException ex) { catch (FileNotFoundException ex) {
// No additional metadata // No additional metadata
return metadata; return metadata;
} }
catch (Exception ex) { catch (Exception ex) {
logWarning("Unable to merge additional-spring-configuration-metadata.json"); logWarning("Unable to merge additional metadata");
logWarning(getStackTrace(ex)); logWarning(getStackTrace(ex));
return metadata; return metadata;
} }
@ -383,26 +369,6 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
return writer.toString(); return writer.toString();
} }
private InputStream getAdditionalMetadata() throws IOException {
// Most build systems will have copied the file to the class output location
FileObject fileObject = this.processingEnv.getFiler().createResource(
StandardLocation.CLASS_OUTPUT, "",
"META-INF/additional-spring-configuration-metadata.json");
File file = new File(fileObject.toUri());
if (!file.exists()) {
// Gradle keeps things separate
String path = file.getPath();
int index = path.lastIndexOf(CLASSES_FOLDER);
if (index >= 0) {
path = path.substring(0, index) + RESOURCES_FOLDER
+ path.substring(index + CLASSES_FOLDER.length());
file = new File(path);
}
}
return (file.exists() ? new FileInputStream(file) : fileObject.toUri().toURL()
.openStream());
}
private void logWarning(String msg) { private void logWarning(String msg) {
this.processingEnv.getMessager().printMessage(Kind.WARNING, msg); this.processingEnv.getMessager().printMessage(Kind.WARNING, msg);
} }

@ -0,0 +1,109 @@
/*
* 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.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
/**
* {@code BuildHandler} that provides incremental build support by merging the metadata
* from the current incremental build with any existing metadata.
*
* @author Andy Wilkinson
* @author Kris De Volder
* @since 1.2.2
*/
public class IncrementalBuildHandler extends StandardBuildHandler {
private final Set<String> processedSourceTypes = new HashSet<String>();
private final ProcessingEnvironment processingEnvironment;
private final ConfigurationMetadata existingMetadata;
private final TypeUtils typeUtils;
/**
* Creates a new {@code IncrementalBuildTracker} that will merge the metadata produced
* by an incremental build with the given {@code existingMetadata}.
*
* @param processingEnvironment The processing environment of the build
* @param existingMetadata The existing metadata
*/
public IncrementalBuildHandler(ProcessingEnvironment processingEnvironment,
ConfigurationMetadata existingMetadata) {
this.existingMetadata = existingMetadata;
this.processingEnvironment = processingEnvironment;
this.typeUtils = new TypeUtils(processingEnvironment);
}
@Override
public void processing(RoundEnvironment environment) {
for (Element element : environment.getRootElements()) {
markAsProcessed(element);
}
}
@Override
public ConfigurationMetadata produceMetadata() {
ConfigurationMetadata metadata = super.produceMetadata();
mergeExistingMetadata(metadata);
return metadata;
}
private void markAsProcessed(Element element) {
if (element instanceof TypeElement) {
this.processedSourceTypes.add(this.typeUtils.getType(element));
}
}
private void mergeExistingMetadata(ConfigurationMetadata metadata) {
List<ItemMetadata> items = this.existingMetadata.getItems();
for (ItemMetadata oldItem : items) {
if (shouldBeMerged(oldItem)) {
metadata.add(oldItem);
}
}
}
private boolean shouldBeMerged(ItemMetadata itemMetadata) {
String sourceType = itemMetadata.getSourceType();
if (sourceType == null || deletedInCurrentBuild(sourceType)
|| processedInCurrentBuild(sourceType)) {
return false;
}
return true;
}
private boolean deletedInCurrentBuild(String sourceType) {
return this.processingEnvironment.getElementUtils().getTypeElement(sourceType) == null;
}
private boolean processedInCurrentBuild(String sourceType) {
return this.processedSourceTypes.contains(sourceType);
}
}

@ -0,0 +1,122 @@
/*
* Copyright 2012-2015 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.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.annotation.processing.ProcessingEnvironment;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
/**
* A {@code MetadataStore} is responsible for the storage of metadata on the filesystem
*
* @author Andy Wilkinson
* @since 1.2.2
*/
public class MetadataStore {
static final String METADATA_PATH = "META-INF/spring-configuration-metadata.json";
private static final String ADDITIONAL_METADATA_PATH = "META-INF/additional-spring-configuration-metadata.json";
private static final String RESOURCES_FOLDER = "resources";
private static final String CLASSES_FOLDER = "classes";
private final ProcessingEnvironment environment;
public MetadataStore(ProcessingEnvironment environment) {
this.environment = environment;
}
public ConfigurationMetadata readMetadata() {
try {
return readMetadata(getMetadataResource().openInputStream());
}
catch (IOException ex) {
return null;
}
}
public void writeMetadata(ConfigurationMetadata metadata) throws IOException {
if (!metadata.getItems().isEmpty()) {
OutputStream outputStream = createMetadataResource().openOutputStream();
try {
new JsonMarshaller().write(metadata, outputStream);
}
finally {
outputStream.close();
}
}
}
public ConfigurationMetadata readAdditionalMetadata() throws IOException {
return readMetadata(getAdditionalMetadataStream());
}
private ConfigurationMetadata readMetadata(InputStream in) throws IOException {
try {
return new JsonMarshaller().read(in);
}
catch (IOException ex) {
return null;
}
finally {
in.close();
}
}
private FileObject getMetadataResource() throws IOException {
FileObject resource = this.environment.getFiler().getResource(
StandardLocation.CLASS_OUTPUT, "", METADATA_PATH);
return resource;
}
private FileObject createMetadataResource() throws IOException {
FileObject resource = this.environment.getFiler().createResource(
StandardLocation.CLASS_OUTPUT, "", METADATA_PATH);
return resource;
}
private InputStream getAdditionalMetadataStream() throws IOException {
// Most build systems will have copied the file to the class output location
FileObject fileObject = this.environment.getFiler().getResource(
StandardLocation.CLASS_OUTPUT, "", ADDITIONAL_METADATA_PATH);
File file = new File(fileObject.toUri());
if (!file.exists()) {
// Gradle keeps things separate
String path = file.getPath();
int index = path.lastIndexOf(CLASSES_FOLDER);
if (index >= 0) {
path = path.substring(0, index) + RESOURCES_FOLDER
+ path.substring(index + CLASSES_FOLDER.length());
file = new File(path);
}
}
return (file.exists() ? new FileInputStream(file) : fileObject.toUri().toURL()
.openStream());
}
}

@ -0,0 +1,58 @@
/*
* 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 javax.annotation.processing.RoundEnvironment;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
/**
* Standard implementation of {@code BuildHandler} that handles the state of a single
* build.
*
* @author Andy Wilkinson
* @since 1.2.2
*/
public class StandardBuildHandler implements BuildHandler {
private final ConfigurationMetadata metadata = new ConfigurationMetadata();
@Override
public void addGroup(String name, String type, String sourceType, String sourceMethod) {
this.metadata.add(ItemMetadata.newGroup(name, type, sourceType, sourceMethod));
}
@Override
public void addProperty(String prefix, String name, String type, String sourceType,
String sourceMethod, String description, Object defaultValue,
boolean deprecated) {
this.metadata.add(ItemMetadata.newProperty(prefix, name, type, sourceType,
sourceMethod, description, defaultValue, deprecated));
}
@Override
public void processing(RoundEnvironment environment) {
}
@Override
public ConfigurationMetadata produceMetadata() {
return this.metadata;
}
}

@ -17,14 +17,9 @@
package org.springframework.boot.configurationprocessor; package org.springframework.boot.configurationprocessor;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import org.junit.Before; import org.junit.Before;
@ -32,7 +27,9 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder; import org.junit.rules.TemporaryFolder;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller; import org.springframework.boot.configurationsample.incremental.BarProperties;
import org.springframework.boot.configurationsample.incremental.FooProperties;
import org.springframework.boot.configurationsample.incremental.RenamedBarProperties;
import org.springframework.boot.configurationsample.lombok.LombokExplicitProperties; import org.springframework.boot.configurationsample.lombok.LombokExplicitProperties;
import org.springframework.boot.configurationsample.lombok.LombokSimpleDataProperties; import org.springframework.boot.configurationsample.lombok.LombokSimpleDataProperties;
import org.springframework.boot.configurationsample.lombok.LombokSimpleProperties; import org.springframework.boot.configurationsample.lombok.LombokSimpleProperties;
@ -57,9 +54,12 @@ import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsGroup; import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsGroup;
import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsProperty; import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsProperty;
import static org.springframework.boot.configurationprocessor.MetadataStore.METADATA_PATH;
/** /**
* Tests for {@link ConfigurationMetadataAnnotationProcessor}. * Tests for {@link ConfigurationMetadataAnnotationProcessor}.
@ -67,6 +67,7 @@ import static org.springframework.boot.configurationprocessor.ConfigurationMetad
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Phillip Webb * @author Phillip Webb
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Kris De Volder
*/ */
public class ConfigurationMetadataAnnotationProcessorTests { public class ConfigurationMetadataAnnotationProcessorTests {
@ -349,7 +350,80 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata, assertThat(metadata,
containsProperty("foo", String.class) containsProperty("foo", String.class)
.fromSource(AdditionalMetadata.class)); .fromSource(AdditionalMetadata.class));
}
@Test
public void incrementalBuild() throws Exception {
TestProject project = new TestProject(this.temporaryFolder, FooProperties.class,
BarProperties.class);
assertFalse(project.getOutputFile(METADATA_PATH).exists());
ConfigurationMetadata metadata = project.fullBuild();
assertTrue(project.getOutputFile(METADATA_PATH).exists());
assertThat(metadata,
containsProperty("foo.counter").fromSource(FooProperties.class));
assertThat(metadata,
containsProperty("bar.counter").fromSource(BarProperties.class));
metadata = project.incrementalBuild(BarProperties.class);
assertThat(metadata,
containsProperty("foo.counter").fromSource(FooProperties.class));
assertThat(metadata,
containsProperty("bar.counter").fromSource(BarProperties.class));
project.addSourceCode(BarProperties.class,
BarProperties.class.getResourceAsStream("BarProperties.snippet"));
metadata = project.incrementalBuild(BarProperties.class);
assertThat(metadata, containsProperty("bar.extra"));
assertThat(metadata, containsProperty("foo.counter"));
assertThat(metadata, containsProperty("bar.counter"));
project.revert(BarProperties.class);
metadata = project.incrementalBuild(BarProperties.class);
assertThat(metadata, not(containsProperty("bar.extra")));
assertThat(metadata, containsProperty("foo.counter"));
assertThat(metadata, containsProperty("bar.counter"));
}
@Test
public void incremenalBuildAnnotationRemoved() throws Exception {
TestProject project = new TestProject(this.temporaryFolder, FooProperties.class,
BarProperties.class);
ConfigurationMetadata metadata = project.fullBuild();
assertThat(metadata, containsProperty("foo.counter"));
assertThat(metadata, containsProperty("bar.counter"));
project.replaceText(BarProperties.class, "@ConfigurationProperties",
"//@ConfigurationProperties");
metadata = project.incrementalBuild(BarProperties.class);
assertThat(metadata, containsProperty("foo.counter"));
assertThat(metadata, not(containsProperty("bar.counter")));
}
@Test
public void incremenalBuildTypeRenamed() throws Exception {
TestProject project = new TestProject(this.temporaryFolder, FooProperties.class,
BarProperties.class);
ConfigurationMetadata metadata = project.fullBuild();
assertThat(metadata,
containsProperty("foo.counter").fromSource(FooProperties.class));
assertThat(metadata,
containsProperty("bar.counter").fromSource(BarProperties.class));
assertThat(metadata,
not(containsProperty("bar.counter")
.fromSource(RenamedBarProperties.class)));
project.delete(BarProperties.class);
project.add(RenamedBarProperties.class);
metadata = project.incrementalBuild(RenamedBarProperties.class);
assertThat(metadata,
containsProperty("foo.counter").fromSource(FooProperties.class));
assertThat(metadata,
not(containsProperty("bar.counter").fromSource(BarProperties.class)));
assertThat(metadata,
containsProperty("bar.counter").fromSource(RenamedBarProperties.class));
} }
private void assertSimpleLombokProperties(ConfigurationMetadata metadata, private void assertSimpleLombokProperties(ConfigurationMetadata metadata,
@ -368,59 +442,12 @@ public class ConfigurationMetadataAnnotationProcessorTests {
} }
private ConfigurationMetadata compile(Class<?>... types) throws IOException { private ConfigurationMetadata compile(Class<?>... types) throws IOException {
TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor(); TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor(
this.compiler.getOutputLocation());
this.compiler.getTask(types).call(processor); this.compiler.getTask(types).call(processor);
return processor.getMetadata(); return processor.getMetadata();
} }
@SupportedAnnotationTypes({ "*" })
@SupportedSourceVersion(SourceVersion.RELEASE_6)
private 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) {
super.writeMetaData(metadata);
try {
File metadataFile = new File(
ConfigurationMetadataAnnotationProcessorTests.this.compiler
.getOutputLocation(),
"META-INF/spring-configuration-metadata.json");
if (metadataFile.isFile()) {
this.metadata = new JsonMarshaller().read(new FileInputStream(
metadataFile));
}
else {
this.metadata = metadata;
}
}
catch (IOException e) {
throw new RuntimeException("Failed to read metadata from disk", e);
}
}
public ConfigurationMetadata getMetadata() {
return this.metadata;
}
}
private static class AdditionalMetadata { private static class AdditionalMetadata {
} }

@ -19,6 +19,7 @@ package org.springframework.boot.configurationprocessor;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection;
import javax.annotation.processing.Processor; import javax.annotation.processing.Processor;
import javax.tools.JavaCompiler; import javax.tools.JavaCompiler;
@ -39,6 +40,8 @@ import org.junit.rules.TemporaryFolder;
*/ */
public class TestCompiler { public class TestCompiler {
public static final File ORIGINAL_SOURCE_FOLDER = new File("src/test/java");
private final JavaCompiler compiler; private final JavaCompiler compiler;
private final StandardJavaFileManager fileManager; private final StandardJavaFileManager fileManager;
@ -59,8 +62,18 @@ public class TestCompiler {
this.fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, temp); this.fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, temp);
} }
public TestCompilationTask getTask(Collection<File> sourceFiles) {
Iterable<? extends JavaFileObject> javaFileObjects = this.fileManager
.getJavaFileObjectsFromFiles(sourceFiles);
return getTask(javaFileObjects);
}
public TestCompilationTask getTask(Class<?>... types) { public TestCompilationTask getTask(Class<?>... types) {
Iterable<? extends JavaFileObject> javaFileObjects = getJavaFileObjects(types); Iterable<? extends JavaFileObject> javaFileObjects = getJavaFileObjects(types);
return getTask(javaFileObjects);
}
private TestCompilationTask getTask(Iterable<? extends JavaFileObject> javaFileObjects) {
return new TestCompilationTask(this.compiler.getTask(null, this.fileManager, return new TestCompilationTask(this.compiler.getTask(null, this.fileManager,
null, null, null, javaFileObjects)); null, null, null, javaFileObjects));
} }
@ -77,8 +90,16 @@ public class TestCompiler {
return this.fileManager.getJavaFileObjects(files); return this.fileManager.getJavaFileObjects(files);
} }
private File getFile(Class<?> type) { protected File getFile(Class<?> type) {
return new File("src/test/java/" + type.getName().replace(".", "/") + ".java"); return new File(getSourceFolder(), sourcePathFor(type));
}
public static String sourcePathFor(Class<?> type) {
return type.getName().replace(".", "/") + ".java";
}
protected File getSourceFolder() {
return ORIGINAL_SOURCE_FOLDER;
} }
/** /**

@ -0,0 +1,86 @@
/*
* Copyright 2012-2015 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.FileInputStream;
import java.io.IOException;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
/**
* @author Stephane Nicoll
* @author Phillip Webb
* @author Andy Wilkinson
* @author Kris De Volder
*/
@SupportedAnnotationTypes({ "*" })
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public 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;
private final File outputLocation;
public TestConfigurationMetadataAnnotationProcessor(File outputLocation) {
this.outputLocation = outputLocation;
}
@Override
protected String configurationPropertiesAnnotation() {
return CONFIGURATION_PROPERTIES_ANNOTATION;
}
@Override
protected String nestedConfigurationPropertyAnnotation() {
return NESTED_CONFIGURATION_PROPERTY_ANNOTATION;
}
@Override
protected ConfigurationMetadata writeMetaData() {
super.writeMetaData();
try {
File metadataFile = new File(this.outputLocation,
"META-INF/spring-configuration-metadata.json");
if (metadataFile.isFile()) {
this.metadata = new JsonMarshaller().read(new FileInputStream(
metadataFile));
}
else {
this.metadata = new ConfigurationMetadata();
}
return this.metadata;
}
catch (IOException e) {
throw new RuntimeException("Failed to read metadata from disk", e);
}
}
public ConfigurationMetadata getMetadata() {
return this.metadata;
}
}

@ -0,0 +1,193 @@
/*
* Copyright 2012-2015 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.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import org.junit.Assert;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.configurationprocessor.TestCompiler.TestCompilationTask;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationsample.ConfigurationProperties;
import org.springframework.boot.configurationsample.NestedConfigurationProperty;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.FileSystemUtils;
import static org.springframework.boot.configurationprocessor.TestCompiler.ORIGINAL_SOURCE_FOLDER;
import static org.springframework.boot.configurationprocessor.TestCompiler.sourcePathFor;
/**
* A TestProject contains a copy of a subset of test sample code.
* <p>
* Why a copy? Because when doing incremental build testing, we need to make modifications
* to the contents of the 'test project'. But we don't want to actually modify the
* original content itself.
*
* @author Kris De Volder
*/
public class TestProject {
private static final Class<?>[] ALWAYS_INCLUDE = { ConfigurationProperties.class,
NestedConfigurationProperty.class };
/**
* Contains copies of the original source so we can modify it safely to test
* incremental builds.
*/
private File sourceFolder;
private TestCompiler compiler;
private Set<File> sourceFiles = new LinkedHashSet<File>();
public TestProject(TemporaryFolder tempFolder, Class<?>... classes)
throws IOException {
this.sourceFolder = tempFolder.newFolder();
this.compiler = new TestCompiler(tempFolder) {
@Override
protected File getSourceFolder() {
return TestProject.this.sourceFolder;
}
};
Set<Class<?>> contents = new HashSet<Class<?>>(Arrays.asList(classes));
contents.addAll(Arrays.asList(ALWAYS_INCLUDE));
copySources(contents);
}
private void copySources(Set<Class<?>> contents) throws IOException {
for (Class<?> klass : contents) {
copySources(klass);
}
}
private void copySources(Class<?> klass) throws IOException {
File original = getOriginalSourceFile(klass);
File target = getSourceFile(klass);
target.getParentFile().mkdirs();
FileCopyUtils.copy(original, target);
this.sourceFiles.add(target);
}
public File getSourceFile(Class<?> klass) {
return new File(this.sourceFolder, sourcePathFor(klass));
}
public ConfigurationMetadata fullBuild() {
TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor(
this.compiler.getOutputLocation());
TestCompilationTask task = this.compiler.getTask(this.sourceFiles);
deleteFolderContents(this.compiler.getOutputLocation());
task.call(processor);
return processor.getMetadata();
}
public ConfigurationMetadata incrementalBuild(Class<?>... toRecompile) {
TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor(
this.compiler.getOutputLocation());
TestCompilationTask task = this.compiler.getTask(toRecompile);
task.call(processor);
return processor.getMetadata();
}
private void deleteFolderContents(File outputFolder) {
FileSystemUtils.deleteRecursively(outputFolder);
outputFolder.mkdirs();
}
/**
* Retrieve File relative to project's output folder.
*/
public File getOutputFile(String relativePath) {
Assert.assertFalse(new File(relativePath).isAbsolute());
return new File(this.compiler.getOutputLocation(), relativePath);
}
/**
* Add source code at the end of file, just before last '}'
*/
public void addSourceCode(Class<?> target, InputStream snippetStream)
throws Exception {
File targetFile = getSourceFile(target);
String contents = getContents(targetFile);
int insertAt = contents.lastIndexOf('}');
String additionalSource = FileCopyUtils.copyToString(new InputStreamReader(
snippetStream));
contents = contents.substring(0, insertAt) + additionalSource
+ contents.substring(insertAt);
putContents(targetFile, contents);
}
/**
* Delete source file for given class from project.
*/
public void delete(Class<?> klass) {
File target = getSourceFile(klass);
target.delete();
this.sourceFiles.remove(target);
}
/**
* Restore source code of given class to its original contents.
*/
public void revert(Class<?> klass) throws IOException {
Assert.assertTrue(getSourceFile(klass).exists());
copySources(klass);
}
/**
* Add source code of given class to this project.
*/
public void add(Class<?> klass) throws IOException {
Assert.assertFalse(getSourceFile(klass).exists());
copySources(klass);
}
public void replaceText(Class<?> klass, String find, String replace) throws Exception {
File target = getSourceFile(klass);
String contents = getContents(target);
contents = contents.replace(find, replace);
putContents(target, contents);
}
/**
* Find the 'original' source code for given test class. Clients or subclasses should
* have no need to know about these. They should work only with the copied source
* code.
*/
private File getOriginalSourceFile(Class<?> klass) {
return new File(ORIGINAL_SOURCE_FOLDER, sourcePathFor(klass));
}
private static void putContents(File targetFile, String contents)
throws FileNotFoundException, IOException, UnsupportedEncodingException {
FileCopyUtils.copy(new StringReader(contents), new FileWriter(targetFile));
}
private static String getContents(File file) throws Exception {
return FileCopyUtils.copyToString(new FileReader(file));
}
}

@ -0,0 +1,56 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationsample.incremental;
import org.springframework.boot.configurationsample.ConfigurationProperties;
@ConfigurationProperties("bar")
public class BarProperties {
private String name;
private String description;
/**
* A nice counter description.
*/
private Integer counter = 0;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getCounter() {
return this.counter;
}
public void setCounter(Integer counter) {
this.counter = counter;
}
}

@ -0,0 +1,57 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationsample.incremental;
import org.springframework.boot.configurationsample.ConfigurationProperties;
@ConfigurationProperties("foo")
public class FooProperties {
private String name;
private String description;
/**
* A nice counter description.
*/
private Integer counter = 0;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getCounter() {
return this.counter;
}
public void setCounter(Integer counter) {
this.counter = counter;
}
}

@ -0,0 +1,56 @@
/*
* Copyright 2012-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationsample.incremental;
import org.springframework.boot.configurationsample.ConfigurationProperties;
@ConfigurationProperties("bar")
public class RenamedBarProperties {
private String name;
private String description;
/**
* A nice counter description.
*/
private Integer counter = 0;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public Integer getCounter() {
return this.counter;
}
public void setCounter(Integer counter) {
this.counter = counter;
}
}

@ -0,0 +1,9 @@
private String extra;
public String getExtra() {
return extra;
}
public void setExtra(String extra) {
this.extra = extra;
}
Loading…
Cancel
Save