diff --git a/spring-boot-tools/spring-boot-configuration-processor/pom.xml b/spring-boot-tools/spring-boot-configuration-processor/pom.xml index c3649bff5c7..190cd25e23b 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/ConfigurationMetadataAnnotationProcessor.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessor.java index cd8812c02b6..ee810139b61 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 @@ -25,7 +25,9 @@ import java.io.OutputStream; import java.io.PrintWriter; import java.io.StringWriter; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; @@ -62,11 +64,14 @@ import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller; * * @author Stephane Nicoll * @author Phillip Webb + * @author Kris De Volder * @since 1.2.0 */ @SupportedAnnotationTypes({ "*" }) public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor { + public static final String METADATA_PATH = "META-INF/spring-configuration-metadata.json"; + static final String CONFIGURATION_PROPERTIES_ANNOTATION = "org.springframework.boot." + "context.properties.ConfigurationProperties"; @@ -85,6 +90,17 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor private ConfigurationMetadata metadata; + /** + * On incremental builds, holds the 'old' metadata (created by the previous build). + */ + private ConfigurationMetadata oldmetadata; + + /** + * On incremental builds, keeps track of the types that where presented to the + * processor. This includes types that are not annotated. + */ + protected Set processedSourceTypes; + private TypeUtils typeUtils; private FieldValuesParser fieldValuesParser; @@ -109,6 +125,10 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor super.init(env); this.metadata = new ConfigurationMetadata(); this.typeUtils = new TypeUtils(env); + this.oldmetadata = readMetadata(); + if (isIncremental()) { + this.processedSourceTypes = new HashSet(); + } try { this.fieldValuesParser = new JavaCompilerFieldValuesParser(env); } @@ -119,10 +139,37 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor } } + protected boolean isIncremental() { + return this.oldmetadata != null; + } + + protected boolean isDeleted(String sourceType) { + return this.processingEnv.getElementUtils().getTypeElement(sourceType) == null; + } + + protected boolean isProcessed(String sourceType) { + return this.processedSourceTypes.contains(sourceType); + } + + /** + * Called during incremental build on all the 'root elements' that are being presented + * to the processor. + */ + protected void markAsProcessed(Element element) { + if (element instanceof TypeElement) { + this.processedSourceTypes.add(this.typeUtils.getType(element)); + } + } + @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { Elements elementUtils = this.processingEnv.getElementUtils(); + if (isIncremental()) { + for (Element element : roundEnv.getRootElements()) { + markAsProcessed(element); + } + } for (Element element : roundEnv.getElementsAnnotatedWith(elementUtils .getTypeElement(configurationPropertiesAnnotation()))) { processElement(element); @@ -328,16 +375,19 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor return values; } - protected void writeMetaData(ConfigurationMetadata metadata) { + protected ConfigurationMetadata writeMetaData(ConfigurationMetadata metadata) { metadata = mergeAdditionalMetadata(metadata); + if (isIncremental()) { + mergeOldMetadata(metadata); + } if (!metadata.getItems().isEmpty()) { try { FileObject resource = this.processingEnv.getFiler().createResource( - StandardLocation.CLASS_OUTPUT, "", - "META-INF/spring-configuration-metadata.json"); + StandardLocation.CLASS_OUTPUT, "", METADATA_PATH); OutputStream outputStream = resource.openOutputStream(); try { new JsonMarshaller().write(metadata, outputStream); + return metadata; } finally { outputStream.close(); @@ -347,6 +397,42 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor throw new IllegalStateException(ex); } } + return null; + } + + protected ConfigurationMetadata readMetadata() { + try { + FileObject resource = this.processingEnv.getFiler().getResource( + StandardLocation.CLASS_OUTPUT, "", METADATA_PATH); + InputStream in = resource.openInputStream(); + try { + ConfigurationMetadata data = new ConfigurationMetadata(); + data.addAll(new JsonMarshaller().read(in)); + if (!data.getItems().isEmpty()) { + return data; + } + } + finally { + in.close(); + } + } + catch (IOException e) { + // no 'old' metadata + } + return null; + } + + private void mergeOldMetadata(ConfigurationMetadata metadata) { + List items = this.oldmetadata.getItems(); + for (ItemMetadata oldItem : items) { + String sourceType = oldItem.getSourceType(); + if (sourceType == null || isProcessed(sourceType) || isDeleted(sourceType)) { + // skip + } + else { + metadata.add(oldItem); + } + } } private ConfigurationMetadata mergeAdditionalMetadata(ConfigurationMetadata metadata) { diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/BuildResult.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/BuildResult.java new file mode 100644 index 00000000000..3fc1e870c75 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/BuildResult.java @@ -0,0 +1,47 @@ +/* + * 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.util.Set; + +import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; + +/** + * Data object containing information about a finished build. + * + * @author Kris De Volder + */ +public class BuildResult { + + public final ConfigurationMetadata metadata; + + public final Set processedTypes; + + public final boolean isIncremental; + + public BuildResult(boolean isIncremental, ConfigurationMetadata metadata, + Set processedTypes) { + this.isIncremental = isIncremental; + this.metadata = metadata; + this.processedTypes = processedTypes; + } + + public BuildResult(TestConfigurationMetadataAnnotationProcessor processor) { + this(processor.isIncremental(), processor.getMetadata(), + processor.processedSourceTypes); + } + +} 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 e898ef8ad4a..207a397c6af 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,7 +54,10 @@ 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.ConfigurationMetadataAnnotationProcessor.METADATA_PATH; import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsGroup; import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsProperty; @@ -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 { @@ -80,6 +81,92 @@ public class ConfigurationMetadataAnnotationProcessorTests { this.compiler = new TestCompiler(this.temporaryFolder); } + @Test + public void incrementalBuild() throws Exception { + TestProject project = new TestProject(this.temporaryFolder, FooProperties.class, + BarProperties.class); + assertFalse(project.getOutputFile(METADATA_PATH).exists()); + + BuildResult r = project.fullBuild(); + assertFalse(r.isIncremental); + assertTrue(project.getOutputFile(METADATA_PATH).exists()); + + assertThat(r.metadata, + containsProperty("foo.counter").fromSource(FooProperties.class)); + assertThat(r.metadata, + containsProperty("bar.counter").fromSource(BarProperties.class)); + + r = project.incrementalBuild(BarProperties.class); + assertTrue(r.isIncremental); + assertTrue(r.processedTypes.contains(BarProperties.class.getName())); + assertFalse(r.processedTypes.contains(FooProperties.class.getName())); + + assertThat(r.metadata, + containsProperty("foo.counter").fromSource(FooProperties.class)); + assertThat(r.metadata, + containsProperty("bar.counter").fromSource(BarProperties.class)); + + assertTrue(r.processedTypes.contains(BarProperties.class.getName())); + assertFalse(r.processedTypes.contains(FooProperties.class.getName())); + + project.addSourceCode(BarProperties.class, " private String extra;\n" + " \n" + + " public String getExtra() {\n" + " return extra;\n" + " }\n" + "\n" + + " public void setExtra(String extra) {\n" + " this.extra = extra;\n" + + " }\n"); + r = project.incrementalBuild(BarProperties.class); + assertTrue(r.isIncremental); + assertThat(r.metadata, containsProperty("bar.extra")); + assertThat(r.metadata, containsProperty("foo.counter")); + assertThat(r.metadata, containsProperty("bar.counter")); + + project.revert(BarProperties.class); + r = project.incrementalBuild(BarProperties.class); + assertTrue(r.isIncremental); + assertThat(r.metadata, not(containsProperty("bar.extra"))); + assertThat(r.metadata, containsProperty("foo.counter")); + assertThat(r.metadata, containsProperty("bar.counter")); + } + + @Test + public void incremenalBuildAnnotationRemoved() throws Exception { + TestProject project = new TestProject(this.temporaryFolder, FooProperties.class, + BarProperties.class); + BuildResult r = project.fullBuild(); + assertThat(r.metadata, containsProperty("foo.counter")); + assertThat(r.metadata, containsProperty("bar.counter")); + + project.replaceText(BarProperties.class, "@ConfigurationProperties", + "//@ConfigurationProperties"); + r = project.incrementalBuild(BarProperties.class); + assertThat(r.metadata, containsProperty("foo.counter")); + assertThat(r.metadata, not(containsProperty("bar.counter"))); + } + + @Test + public void incremenalBuildTypeRenamed() throws Exception { + TestProject project = new TestProject(this.temporaryFolder, FooProperties.class, + BarProperties.class); + BuildResult r = project.fullBuild(); + assertThat(r.metadata, + containsProperty("foo.counter").fromSource(FooProperties.class)); + assertThat(r.metadata, + containsProperty("bar.counter").fromSource(BarProperties.class)); + assertThat(r.metadata, + not(containsProperty("bar.counter") + .fromSource(RenamedBarProperties.class))); + + project.delete(BarProperties.class); + project.add(RenamedBarProperties.class); + r = project.incrementalBuild(RenamedBarProperties.class); + assertThat(r.metadata, + containsProperty("foo.counter").fromSource(FooProperties.class)); + assertThat(r.metadata, + not(containsProperty("bar.counter").fromSource(BarProperties.class))); + assertThat(r.metadata, + containsProperty("bar.counter").fromSource(RenamedBarProperties.class)); + + } + @Test public void notAnnotated() throws Exception { ConfigurationMetadata metadata = compile(NotAnnotated.class); @@ -368,59 +455,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 871fd88b87a..a66b7b71869 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 00000000000..4964d7506ba --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestConfigurationMetadataAnnotationProcessor.java @@ -0,0 +1,95 @@ +/* + * 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.util.Set; + +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(ConfigurationMetadata metadata) { + super.writeMetaData(metadata); + 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 = metadata; + } + return this.metadata; + } + catch (IOException e) { + throw new RuntimeException("Failed to read metadata from disk", e); + } + } + + public ConfigurationMetadata getMetadata() { + return this.metadata; + } + + public Set getProcessedTypes() { + return this.processedSourceTypes; + } + + @Override + public boolean isIncremental() { + return super.isIncremental(); + } +} \ 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 00000000000..d17a9b1a871 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/TestProject.java @@ -0,0 +1,187 @@ +/* + * 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.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.configurationsample.ConfigurationProperties; +import org.springframework.boot.configurationsample.NestedConfigurationProperty; +import org.springframework.boot.configurationsample.incremental.BarProperties; +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 BuildResult fullBuild() { + TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor( + this.compiler.getOutputLocation()); + TestCompilationTask task = this.compiler.getTask(this.sourceFiles); + deleteFolderContents(this.compiler.getOutputLocation()); + task.call(processor); + return new BuildResult(processor); + } + + public BuildResult incrementalBuild(Class... toRecompile) { + TestConfigurationMetadataAnnotationProcessor processor = new TestConfigurationMetadataAnnotationProcessor( + this.compiler.getOutputLocation()); + TestCompilationTask task = this.compiler.getTask(toRecompile); + task.call(processor); + return new BuildResult(processor); + } + + 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, String text) throws Exception { + File targetFile = getSourceFile(target); + String contents = getContents(targetFile); + int insertAt = contents.lastIndexOf('}'); + contents = contents.substring(0, insertAt) + text + 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 00000000000..f0f143a1625 --- /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 00000000000..2a046b3e12a --- /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 00000000000..8dc73f04e53 --- /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; + } +}