Add a defaultValue attribute to config meta-data

Update `ConfigurationMetadataAnnotationProcessor` to include the
'defaultValue' of a property when possible. For example the
'defaultValue' or 'server.port' is '8080'.

Default values are detected by inspecting the field assignments of
@ConfigurationProperties items. In order to detect field values some
internals of the Java compiler are used. To save a dependency on
'tools.jar' internal javac classes are accessed using reflection.

See gh-1001
This commit is contained in:
Phillip Webb
2014-10-28 15:09:09 -07:00
parent 884c058e57
commit c73adcd198
18 changed files with 864 additions and 25 deletions

View File

@@ -71,9 +71,10 @@ public class ConfigurationMetadataAnnotationProcessorTests {
assertThat(metadata, containsGroup("simple").fromSource(SimpleProperties.class));
assertThat(
metadata,
containsProperty("simple.the-name", String.class).fromSource(
SimpleProperties.class).withDescription(
"The name of this simple properties."));
containsProperty("simple.the-name", String.class)
.fromSource(SimpleProperties.class)
.withDescription("The name of this simple properties.")
.withDefaultValue("boot"));
assertThat(
metadata,
containsProperty("simple.flag", Boolean.class).fromSource(
@@ -171,7 +172,8 @@ public class ConfigurationMetadataAnnotationProcessorTests {
@Test
public void simpleMethodConfig() throws Exception {
ConfigurationMetadata metadata = compile(SimpleMethodConfig.class);
assertThat(metadata, containsGroup("foo").fromSource(SimpleMethodConfig.class));
assertThat(metadata, containsGroup("foo")
.fromSource(SimpleMethodConfig.class));
assertThat(
metadata,
containsProperty("foo.name", String.class).fromSource(

View File

@@ -66,17 +66,20 @@ public class ConfigurationMetadataMatchers {
private final String description;
private final Object defaultValue;
public ContainsItemMatcher(ItemType itemType, String name) {
this(itemType, name, null, null, null);
this(itemType, name, null, null, null, null);
}
public ContainsItemMatcher(ItemType itemType, String name, String type,
Class<?> sourceType, String description) {
Class<?> sourceType, String description, Object defaultValue) {
this.itemType = itemType;
this.name = name;
this.type = type;
this.sourceType = sourceType;
this.description = description;
this.defaultValue = defaultValue;
}
@Override
@@ -93,6 +96,10 @@ public class ConfigurationMetadataMatchers {
&& !this.sourceType.getName().equals(itemMetadata.getSourceType())) {
return false;
}
if (this.defaultValue != null
&& !this.defaultValue.equals(itemMetadata.getDefaultValue())) {
return false;
}
if (this.description != null
&& !this.description.equals(itemMetadata.getDescription())) {
return false;
@@ -121,6 +128,9 @@ public class ConfigurationMetadataMatchers {
if (this.sourceType != null) {
description.appendText(" sourceType ").appendValue(this.sourceType);
}
if (this.defaultValue != null) {
description.appendText(" defaultValue ").appendValue(this.defaultValue);
}
if (this.description != null) {
description.appendText(" description ").appendValue(this.description);
}
@@ -128,22 +138,27 @@ public class ConfigurationMetadataMatchers {
public ContainsItemMatcher ofType(Class<?> dataType) {
return new ContainsItemMatcher(this.itemType, this.name, dataType.getName(),
this.sourceType, this.description);
this.sourceType, this.description, this.defaultValue);
}
public ContainsItemMatcher ofDataType(String dataType) {
return new ContainsItemMatcher(this.itemType, this.name, dataType,
this.sourceType, this.description);
this.sourceType, this.description, this.defaultValue);
}
public ContainsItemMatcher fromSource(Class<?> sourceType) {
return new ContainsItemMatcher(this.itemType, this.name, this.type,
sourceType, this.description);
sourceType, this.description, this.defaultValue);
}
public ContainsItemMatcher withDescription(String description) {
return new ContainsItemMatcher(this.itemType, this.name, this.type,
this.sourceType, description);
this.sourceType, description, this.defaultValue);
}
public Matcher<? super ConfigurationMetadata> withDefaultValue(Object defaultValue) {
return new ContainsItemMatcher(this.itemType, this.name, this.type,
this.sourceType, this.description, defaultValue);
}
private ItemMetadata getFirstPropertyWithName(ConfigurationMetadata metadata,

View File

@@ -0,0 +1,124 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationprocessor.fieldvalues;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import org.hamcrest.Matcher;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.configurationprocessor.TestCompiler;
import org.springframework.boot.configurationsample.fieldvalues.FieldValues;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
/**
* Abstract base class for {@link FieldValuesParser} tests.
*
* @author Phillip Webb
*/
public abstract class AbstractFieldValuesProcessorTests {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
protected abstract FieldValuesParser createProcessor(ProcessingEnvironment env);
@Test
public void getFieldValues() throws Exception {
TestProcessor processor = new TestProcessor();
TestCompiler compiler = new TestCompiler(this.temporaryFolder);
compiler.getTask(FieldValues.class).call(processor);
Map<String, Object> values = processor.getValues();
assertThat(values.get("string"), equalToObject("1"));
assertThat(values.get("stringNone"), nullValue());
assertThat(values.get("stringConst"), equalToObject("c"));
assertThat(values.get("bool"), equalToObject(true));
assertThat(values.get("boolNone"), equalToObject(false));
assertThat(values.get("boolConst"), equalToObject(true));
assertThat(values.get("boolObject"), equalToObject(true));
assertThat(values.get("boolObjectNone"), nullValue());
assertThat(values.get("boolObjectConst"), equalToObject(true));
assertThat(values.get("integer"), equalToObject(1));
assertThat(values.get("integerNone"), equalToObject(0));
assertThat(values.get("integerConst"), equalToObject(2));
assertThat(values.get("integerObject"), equalToObject(3));
assertThat(values.get("integerObjectNone"), nullValue());
assertThat(values.get("integerObjectConst"), equalToObject(4));
assertThat(values.get("object"), equalToObject(123));
assertThat(values.get("objectNone"), nullValue());
assertThat(values.get("objectConst"), equalToObject("c"));
assertThat(values.get("objectInstance"), nullValue());
}
private Matcher<Object> equalToObject(Object object) {
return equalTo(object);
}
@SupportedAnnotationTypes({ "org.springframework.boot.configurationsample.ConfigurationProperties" })
@SupportedSourceVersion(SourceVersion.RELEASE_6)
private class TestProcessor extends AbstractProcessor {
private FieldValuesParser processor;
private Map<String, Object> values = new HashMap<String, Object>();
@Override
public synchronized void init(ProcessingEnvironment env) {
this.processor = createProcessor(env);
}
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
if (element instanceof TypeElement) {
try {
this.values.putAll(this.processor
.getFieldValues((TypeElement) element));
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}
}
return false;
}
public Map<String, Object> getValues() {
return this.values;
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationprocessor.fieldvalues.javac;
import javax.annotation.processing.ProcessingEnvironment;
import org.springframework.boot.configurationprocessor.fieldvalues.AbstractFieldValuesProcessorTests;
import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser;
import static org.junit.Assume.assumeNoException;
/**
* Tests for {@link JavaCompilerFieldValuesParser}.
*
* @author Phillip Webb
*/
public class JavaCompilerFieldValuesProcessorTests extends
AbstractFieldValuesProcessorTests {
@Override
protected FieldValuesParser createProcessor(ProcessingEnvironment env) {
try {
return new JavaCompilerFieldValuesParser(env);
}
catch (Throwable ex) {
assumeNoException(ex);
throw new IllegalStateException();
}
}
}

View File

@@ -38,10 +38,11 @@ public class JsonMarshallerTests {
public void marshallAndUnmarshal() throws IOException {
ConfigurationMetadata metadata = new ConfigurationMetadata();
metadata.add(ItemMetadata.newProperty("a", "b", StringBuffer.class.getName(),
InputStream.class.getName(), "sourceMethod", "desc"));
metadata.add(ItemMetadata.newProperty("b.c.d", null, null, null, null, null));
metadata.add(ItemMetadata.newProperty("c", null, null, null, null, null));
metadata.add(ItemMetadata.newProperty("d", null, null, null, null, null));
InputStream.class.getName(), "sourceMethod", "desc", "x"));
metadata.add(ItemMetadata
.newProperty("b.c.d", null, null, null, null, null, null));
metadata.add(ItemMetadata.newProperty("c", null, null, null, null, null, 123));
metadata.add(ItemMetadata.newProperty("d", null, null, null, null, null, true));
metadata.add(ItemMetadata.newGroup("d", null, null, null));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
JsonMarshaller marshaller = new JsonMarshaller();
@@ -51,10 +52,10 @@ public class JsonMarshallerTests {
outputStream.toByteArray()));
assertThat(read,
containsProperty("a.b", StringBuffer.class).fromSource(InputStream.class)
.withDescription("desc"));
.withDescription("desc").withDefaultValue("x"));
assertThat(read, containsProperty("b.c.d"));
assertThat(read, containsProperty("c"));
assertThat(read, containsProperty("d"));
assertThat(read, containsProperty("c").withDefaultValue(123));
assertThat(read, containsProperty("d").withDefaultValue(true));
assertThat(read, containsGroup("d"));
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationsample.fieldvalues;
import org.springframework.boot.configurationsample.ConfigurationProperties;
/**
* Sample object containing fields with initial values.
*
* @author Phillip Webb
*/
@SuppressWarnings("unused")
@ConfigurationProperties
public class FieldValues {
private static final String STRING_CONST = "c";
private static final boolean BOOLEAN_CONST = true;
private static final Boolean BOOLEAN_OBJ_CONST = true;
private static final int INTEGER_CONST = 2;
private static final Integer INTEGER_OBJ_CONST = 4;
private String string = "1";
private String stringNone;
private String stringConst = STRING_CONST;
private boolean bool = true;
private boolean boolNone;
private boolean boolConst = BOOLEAN_CONST;
private Boolean boolObject = Boolean.TRUE;
private Boolean boolObjectNone;
private Boolean boolObjectConst = BOOLEAN_OBJ_CONST;
private int integer = 1;
private int integerNone;
private int integerConst = INTEGER_CONST;
private Integer integerObject = 3;
private Integer integerObjectNone;
private Integer integerObjectConst = INTEGER_OBJ_CONST;
private Object object = 123;
private Object objectNone;
private Object objectConst = STRING_CONST;
private Object objectInstance = new StringBuffer();
}