diff --git a/spring-boot-tools/spring-boot-configuration-processor/pom.xml b/spring-boot-tools/spring-boot-configuration-processor/pom.xml index c3649bff5c..190cd25e23 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/pom.xml +++ b/spring-boot-tools/spring-boot-configuration-processor/pom.xml @@ -29,6 +29,10 @@ lombok test + + org.springframework + spring-core + @@ -36,7 +40,7 @@ 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/BuildHandler.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/BuildHandler.java new file mode 100644 index 0000000000..cae8bdfbde --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/BuildHandler.java @@ -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(); + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index cd8812c02b..32830eae99 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java @@ -16,12 +16,8 @@ package org.springframework.boot.configurationprocessor; -import java.io.File; -import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collections; @@ -47,14 +43,10 @@ import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; 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 @@ -62,6 +54,7 @@ import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller; * * @author Stephane Nicoll * @author Phillip Webb + * @author Kris De Volder * @since 1.2.0 */ @SupportedAnnotationTypes({ "*" }) @@ -79,11 +72,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor 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 ConfigurationMetadata metadata; + private BuildHandler buildHandler; private TypeUtils typeUtils; @@ -107,8 +98,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor @Override public synchronized void init(ProcessingEnvironment env) { super.init(env); - this.metadata = new ConfigurationMetadata(); this.typeUtils = new TypeUtils(env); + this.metadataStore = new MetadataStore(env); + this.buildHandler = createBuildHandler(env, this.metadataStore); try { 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 public boolean process(Set annotations, RoundEnvironment roundEnv) { + this.buildHandler.processing(roundEnv); Elements elementUtils = this.processingEnv.getElementUtils(); for (Element element : roundEnv.getElementsAnnotatedWith(elementUtils .getTypeElement(configurationPropertiesAnnotation()))) { processElement(element); } if (roundEnv.processingOver()) { - writeMetaData(this.metadata); + writeMetaData(); } return false; } @@ -149,7 +153,7 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor private void processAnnotatedTypeElement(String prefix, TypeElement 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); } @@ -159,10 +163,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor Element returns = this.processingEnv.getTypeUtils().asElement( element.getReturnType()); if (returns instanceof TypeElement) { - this.metadata.add(ItemMetadata.newGroup(prefix, - this.typeUtils.getType(returns), + this.buildHandler.addGroup(prefix, this.typeUtils.getType(returns), this.typeUtils.getType(element.getEnclosingElement()), - element.toString())); + element.toString()); processTypeElement(prefix, (TypeElement) returns); } } @@ -207,8 +210,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor boolean deprecated = hasDeprecateAnnotation(getter) || hasDeprecateAnnotation(setter) || hasDeprecateAnnotation(element); - this.metadata.add(ItemMetadata.newProperty(prefix, name, dataType, - sourceType, null, description, defaultValue, deprecated)); + this.buildHandler.addProperty(prefix, name, dataType, sourceType, null, + description, defaultValue, deprecated); } } } @@ -235,8 +238,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor Object defaultValue = fieldValues.get(name); boolean deprecated = hasDeprecateAnnotation(field) || hasDeprecateAnnotation(element); - this.metadata.add(ItemMetadata.newProperty(prefix, name, dataType, - sourceType, null, description, defaultValue, deprecated)); + this.buildHandler.addProperty(prefix, name, dataType, sourceType, null, + description, defaultValue, deprecated); } } } @@ -269,9 +272,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor if (returnType != null && returnType instanceof TypeElement && annotation == null && isNested) { String nestedPrefix = ConfigurationMetadata.nestedPrefix(prefix, name); - this.metadata.add(ItemMetadata.newGroup(nestedPrefix, + this.buildHandler.addGroup(nestedPrefix, this.typeUtils.getType(returnType), - this.typeUtils.getType(element), getter.toString())); + this.typeUtils.getType(element), getter.toString()); processTypeElement(nestedPrefix, (TypeElement) returnType); } } @@ -328,50 +331,33 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor return values; } - protected void writeMetaData(ConfigurationMetadata metadata) { + protected ConfigurationMetadata writeMetaData() { + ConfigurationMetadata metadata = this.buildHandler.produceMetadata(); metadata = mergeAdditionalMetadata(metadata); if (!metadata.getItems().isEmpty()) { 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(); - } + this.metadataStore.writeMetadata(metadata); } - catch (Exception ex) { - throw new IllegalStateException(ex); + catch (IOException ex) { + throw new IllegalStateException("Failed to write metadata", ex); } + return metadata; } + return null; } private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata metadata) { try { - InputStream inputStream = getAdditionalMetadata(); - 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(); - } + ConfigurationMetadata merged = new ConfigurationMetadata(metadata); + merged.addAll(this.metadataStore.readAdditionalMetadata()); + return merged; } catch (FileNotFoundException ex) { // No additional metadata return metadata; } catch (Exception ex) { - logWarning("Unable to merge additional-spring-configuration-metadata.json"); + logWarning("Unable to merge additional metadata"); logWarning(getStackTrace(ex)); return metadata; } @@ -383,26 +369,6 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor 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) { this.processingEnv.getMessager().printMessage(Kind.WARNING, msg); } diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/IncrementalBuildHandler.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/IncrementalBuildHandler.java new file mode 100644 index 0000000000..e480a667bc --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/IncrementalBuildHandler.java @@ -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 processedSourceTypes = new HashSet(); + + 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 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); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataStore.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataStore.java new file mode 100644 index 0000000000..154b26ffb9 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/MetadataStore.java @@ -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()); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/StandardBuildHandler.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/StandardBuildHandler.java new file mode 100644 index 0000000000..7e60315b76 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/StandardBuildHandler.java @@ -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; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index e898ef8ad4..f843670be1 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -17,14 +17,9 @@ package org.springframework.boot.configurationprocessor; import java.io.File; -import java.io.FileInputStream; import java.io.FileWriter; 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.JSONObject; import org.junit.Before; @@ -32,7 +27,9 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; 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.LombokSimpleDataProperties; 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.equalTo; import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertFalse; 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.containsProperty; +import static org.springframework.boot.configurationprocessor.MetadataStore.METADATA_PATH; /** * Tests for {@link ConfigurationMetadataAnnotationProcessor}. @@ -67,6 +67,7 @@ import static org.springframework.boot.configurationprocessor.ConfigurationMetad * @author Stephane Nicoll * @author Phillip Webb * @author Andy Wilkinson + * @author Kris De Volder */ public class ConfigurationMetadataAnnotationProcessorTests { @@ -349,7 +350,80 @@ public class ConfigurationMetadataAnnotationProcessorTests { assertThat(metadata, containsProperty("foo", String.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, @@ -368,59 +442,12 @@ public class ConfigurationMetadataAnnotationProcessorTests { } private ConfigurationMetadata compile(Class... types) throws IOException { - TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor(); + TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor( + this.compiler.getOutputLocation()); this.compiler.getTask(types).call(processor); 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 { } 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 index 871fd88b87..a66b7b7186 100644 --- 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 @@ -19,6 +19,7 @@ package org.springframework.boot.configurationprocessor; import java.io.File; import java.io.IOException; import java.util.Arrays; +import java.util.Collection; import javax.annotation.processing.Processor; import javax.tools.JavaCompiler; @@ -39,6 +40,8 @@ import org.junit.rules.TemporaryFolder; */ public class TestCompiler { + public static final File ORIGINAL_SOURCE_FOLDER = new File("src/test/java"); + private final JavaCompiler compiler; private final StandardJavaFileManager fileManager; @@ -59,8 +62,18 @@ public class TestCompiler { this.fileManager.setLocation(StandardLocation.SOURCE_OUTPUT, temp); } + public TestCompilationTask getTask(Collection sourceFiles) { + Iterable javaFileObjects = this.fileManager + .getJavaFileObjectsFromFiles(sourceFiles); + return getTask(javaFileObjects); + } + public TestCompilationTask getTask(Class... types) { Iterable javaFileObjects = getJavaFileObjects(types); + return getTask(javaFileObjects); + } + + private TestCompilationTask getTask(Iterable javaFileObjects) { return new TestCompilationTask(this.compiler.getTask(null, this.fileManager, null, null, null, javaFileObjects)); } @@ -77,8 +90,16 @@ public class TestCompiler { return this.fileManager.getJavaFileObjects(files); } - private File getFile(Class type) { - return new File("src/test/java/" + type.getName().replace(".", "/") + ".java"); + protected File getFile(Class type) { + return new File(getSourceFolder(), sourcePathFor(type)); + } + + public static String sourcePathFor(Class type) { + return type.getName().replace(".", "/") + ".java"; + } + + protected File getSourceFolder() { + return ORIGINAL_SOURCE_FOLDER; } /** diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestConfigurationMetadataAnnotationProcessor.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestConfigurationMetadataAnnotationProcessor.java new file mode 100644 index 0000000000..6ed037db89 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestConfigurationMetadataAnnotationProcessor.java @@ -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; + } + +} \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestProject.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestProject.java new file mode 100644 index 0000000000..23b6eeb578 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestProject.java @@ -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. + *

+ * 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 sourceFiles = new LinkedHashSet(); + + 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> contents = new HashSet>(Arrays.asList(classes)); + contents.addAll(Arrays.asList(ALWAYS_INCLUDE)); + copySources(contents); + } + + private void copySources(Set> 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)); + } +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/incremental/BarProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/incremental/BarProperties.java new file mode 100644 index 0000000000..f0f143a162 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/incremental/BarProperties.java @@ -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; + } +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/incremental/FooProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/incremental/FooProperties.java new file mode 100644 index 0000000000..2a046b3e12 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/incremental/FooProperties.java @@ -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; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/incremental/RenamedBarProperties.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/incremental/RenamedBarProperties.java new file mode 100644 index 0000000000..8dc73f04e5 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/incremental/RenamedBarProperties.java @@ -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; + } +} diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/resources/org/springframework/boot/configurationsample/incremental/BarProperties.snippet b/spring-boot-tools/spring-boot-configuration-processor/src/test/resources/org/springframework/boot/configurationsample/incremental/BarProperties.snippet new file mode 100644 index 0000000000..a9f7727aa6 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/resources/org/springframework/boot/configurationsample/incremental/BarProperties.snippet @@ -0,0 +1,9 @@ + private String extra; + + public String getExtra() { + return extra; + } + + public void setExtra(String extra) { + this.extra = extra; + } \ No newline at end of file