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

@@ -19,6 +19,7 @@ package org.springframework.boot.configurationprocessor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
@@ -38,9 +39,12 @@ import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser;
import org.springframework.boot.configurationprocessor.fieldvalues.javac.JavaCompilerFieldValuesParser;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.metadata.JsonMarshaller;
@@ -64,6 +68,8 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
private TypeUtils typeUtils;
private FieldValuesParser fieldValuesParser;
protected String configurationPropertiesAnnotation() {
return CONFIGURATION_PROPERTIES_ANNOTATION;
}
@@ -73,6 +79,18 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
super.init(env);
this.metadata = new ConfigurationMetadata();
this.typeUtils = new TypeUtils(env);
try {
this.fieldValuesParser = new JavaCompilerFieldValuesParser(env);
}
catch (Throwable ex) {
this.fieldValuesParser = FieldValuesParser.NONE;
logWarning("Field value processing of @ConfigurationProperty meta-data is "
+ "not supported");
}
}
private void logWarning(String msg) {
this.processingEnv.getMessager().printMessage(Kind.WARNING, msg);
}
@Override
@@ -126,12 +144,22 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
private void processTypeElement(String prefix, TypeElement element) {
TypeElementMembers members = new TypeElementMembers(this.processingEnv, element);
processSimpleTypes(prefix, element, members);
Map<String, Object> fieldValues = getFieldValues(element);
processSimpleTypes(prefix, element, members, fieldValues);
processNestedTypes(prefix, element, members);
}
private Map<String, Object> getFieldValues(TypeElement element) {
try {
return this.fieldValuesParser.getFieldValues(element);
}
catch (Exception ex) {
return Collections.emptyMap();
}
}
private void processSimpleTypes(String prefix, TypeElement element,
TypeElementMembers members) {
TypeElementMembers members, Map<String, Object> fieldValues) {
for (Map.Entry<String, ExecutableElement> entry : members.getPublicGetters()
.entrySet()) {
String name = entry.getKey();
@@ -143,8 +171,9 @@ public class ConfigurationMetadataAnnotationProcessor extends AbstractProcessor
String dataType = this.typeUtils.getType(getter.getReturnType());
String sourceType = this.typeUtils.getType(element);
String description = this.typeUtils.getJavaDoc(field);
Object defaultValue = fieldValues.get(name);
this.metadata.add(ItemMetadata.newProperty(prefix, name, dataType,
sourceType, null, description));
sourceType, null, description, defaultValue));
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationprocessor.fieldvalues;
import java.util.Collections;
import java.util.Map;
import javax.lang.model.element.TypeElement;
import org.springframework.boot.configurationprocessor.fieldvalues.javac.JavaCompilerFieldValuesParser;
/**
* Parser which can be used to obtain the field values from an {@link TypeElement}.
*
* @author Phillip Webb
* @since 1.1.2
* @see JavaCompilerFieldValuesParser
*/
public interface FieldValuesParser {
/**
* Implementation of {@link FieldValuesParser} that always returns an empty
* result.
*/
public static final FieldValuesParser NONE = new FieldValuesParser() {
@Override
public Map<String, Object> getFieldValues(TypeElement element) {
return Collections.emptyMap();
}
};
/**
* Return the field values for the given element.
* @param element the element to inspect
* @return a map of field names to values.
* @throws Exception if the values cannot be extracted
*/
Map<String, Object> getFieldValues(TypeElement element) throws Exception;
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationprocessor.fieldvalues.javac;
import java.lang.reflect.Method;
/**
* Reflection based access to {@code com.sun.source.tree.ExpressionTree}.
*
* @author Phillip Webb
* @since 1.2.0
*/
class ExpressionTree extends ReflectionWrapper {
private final Class<?> literalTreeType = findClass("com.sun.source.tree.LiteralTree");
private final Method literalValueMethod = findMethod(this.literalTreeType, "getValue");
public ExpressionTree(Object instance) {
super(instance);
}
public String getKind() throws Exception {
return findMethod("getKind").invoke(getInstance()).toString();
}
public Object getLiteralValue() throws Exception {
if (this.literalTreeType.isAssignableFrom(getInstance().getClass())) {
return this.literalValueMethod.invoke(getInstance());
}
return null;
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationprocessor.fieldvalues.javac;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import org.springframework.boot.configurationprocessor.fieldvalues.FieldValuesParser;
/**
* {@link FieldValuesParser} implementation for the standard Java compiler.
*
* @author Phillip Webb
* @since 1.2.0
*/
public class JavaCompilerFieldValuesParser implements FieldValuesParser {
private final Trees trees;
public JavaCompilerFieldValuesParser(ProcessingEnvironment env) throws Exception {
this.trees = Trees.instance(env);
}
@Override
public Map<String, Object> getFieldValues(TypeElement element) throws Exception {
Tree tree = this.trees.getTree(element);
if (tree != null) {
FieldCollector fieldCollector = new FieldCollector();
tree.accept(fieldCollector);
return fieldCollector.getFieldValues();
}
return Collections.emptyMap();
}
private static class FieldCollector implements TreeVisitor {
private static final Map<String, Class<?>> WRAPPER_TYPES;
static {
Map<String, Class<?>> types = new HashMap<String, Class<?>>();
types.put("boolean", Boolean.class);
types.put(Boolean.class.getName(), Boolean.class);
types.put("byte", Byte.class);
types.put(Byte.class.getName(), Byte.class);
types.put("short", Short.class);
types.put(Short.class.getName(), Short.class);
types.put("int", Integer.class);
types.put(Integer.class.getName(), Integer.class);
types.put("long", Long.class);
types.put(Long.class.getName(), Long.class);
WRAPPER_TYPES = Collections.unmodifiableMap(types);
}
private static final Map<Class<?>, Object> DEFAULT_TYPE_VALUES;
static {
Map<Class<?>, Object> values = new HashMap<Class<?>, Object>();
values.put(Boolean.class, false);
values.put(Byte.class, (byte) 0);
values.put(Short.class, (short) 0);
values.put(Integer.class, 0);
values.put(Long.class, (long) 0);
DEFAULT_TYPE_VALUES = Collections.unmodifiableMap(values);
}
private static final Map<String, Object> WELL_KNOWN_STATIC_FINALS;
static {
Map<String, Object> values = new HashMap<String, Object>();
values.put("Boolean.TRUE", true);
values.put("Boolean.FALSE", false);
WELL_KNOWN_STATIC_FINALS = Collections.unmodifiableMap(values);
}
private final Map<String, Object> fieldValues = new HashMap<String, Object>();
private final Map<String, Object> staticFinals = new HashMap<String, Object>();
@Override
public void visitVariable(VariableTree variable) throws Exception {
Set<Modifier> flags = variable.getModifierFlags();
if (flags.contains(Modifier.STATIC) && flags.contains(Modifier.FINAL)) {
this.staticFinals.put(variable.getName(), getValue(variable));
}
if (!flags.contains(Modifier.FINAL)) {
this.fieldValues.put(variable.getName(), getValue(variable));
}
}
private Object getValue(VariableTree variable) throws Exception {
ExpressionTree initializer = variable.getInitializer();
Class<?> wrapperType = WRAPPER_TYPES.get(variable.getType());
if (initializer != null) {
if (initializer.getLiteralValue() != null) {
return initializer.getLiteralValue();
}
if (initializer.getKind().equals("IDENTIFIER")) {
return this.staticFinals.get(initializer.toString());
}
if (initializer.getKind().equals("MEMBER_SELECT")) {
return WELL_KNOWN_STATIC_FINALS.get(initializer.toString());
}
}
return DEFAULT_TYPE_VALUES.get(wrapperType);
}
public Map<String, Object> getFieldValues() {
return this.fieldValues;
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationprocessor.fieldvalues.javac;
import java.lang.reflect.Method;
/**
* Base class for reflection based wrappers. Used to access internal Java classes without
* needing tools.jar on the classpath.
*
* @author Phillip Webb
* @since 1.2.0
*/
class ReflectionWrapper {
private final Class<?> type;
private final Object instance;
public ReflectionWrapper(Object instance) {
this.type = instance.getClass();
this.instance = instance;
}
public ReflectionWrapper(String type, Object instance) {
this.type = findClass(instance.getClass().getClassLoader(), type);
this.instance = instance;
}
protected final Object getInstance() {
return this.instance;
}
@Override
public String toString() {
return this.instance.toString();
}
protected Class<?> findClass(String name) {
return findClass(getInstance().getClass().getClassLoader(), name);
}
protected Method findMethod(String name, Class<?>... parameterTypes) {
return findMethod(this.type, name, parameterTypes);
}
protected static Class<?> findClass(ClassLoader classLoader, String name) {
try {
return classLoader.loadClass(name);
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(ex);
}
}
protected static Method findMethod(Class<?> type, String name,
Class<?>... parameterTypes) {
try {
return type.getMethod(name, parameterTypes);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationprocessor.fieldvalues.javac;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* Reflection based access to {@code com.sun.source.tree.Tree}.
*
* @author Phillip Webb
* @since 1.2.0
*/
class Tree extends ReflectionWrapper {
private final Class<?> treeVisitorType = findClass("com.sun.source.tree.TreeVisitor");
private final Method acceptMethod = findMethod("accept", this.treeVisitorType,
Object.class);
private final Method GET_CLASS_TREE_MEMBERS = findMethod(
findClass("com.sun.source.tree.ClassTree"), "getMembers");
public Tree(Object instance) {
super("com.sun.source.tree.Tree", instance);
}
public void accept(TreeVisitor visitor) throws Exception {
this.acceptMethod.invoke(getInstance(), Proxy.newProxyInstance(getInstance()
.getClass().getClassLoader(), new Class<?>[] { this.treeVisitorType },
new TreeVisitorInvocationHandler(visitor)), 0);
}
/**
* {@link InvocationHandler} to call the {@link TreeVisitor}.
*/
private class TreeVisitorInvocationHandler implements InvocationHandler {
private TreeVisitor treeVisitor;
public TreeVisitorInvocationHandler(TreeVisitor treeVisitor) {
this.treeVisitor = treeVisitor;
}
@Override
@SuppressWarnings("rawtypes")
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("visitClass")) {
if ((Integer) args[1] == 0) {
Iterable members = (Iterable) Tree.this.GET_CLASS_TREE_MEMBERS
.invoke(args[0]);
for (Object member : members) {
if (member != null) {
Tree.this.acceptMethod.invoke(member, proxy,
((Integer) args[1]) + 1);
}
}
}
}
if (method.getName().equals("visitVariable")) {
this.treeVisitor.visitVariable(new VariableTree(args[0]));
}
return null;
}
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationprocessor.fieldvalues.javac;
/**
* Reflection base alternative for {@code com.sun.source.tree.TreeVisitor}.
*
* @author Phillip Webb
* @since 1.2.0
*/
interface TreeVisitor {
void visitVariable(VariableTree variable) throws Exception;
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationprocessor.fieldvalues.javac;
import java.lang.reflect.Method;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
/**
* Reflection based access to {@code com.sun.source.util.Trees}.
*
* @author Phillip Webb
* @since 1.2.0
*/
class Trees extends ReflectionWrapper {
private Trees(Object instance) {
super(instance);
}
public Tree getTree(Element element) throws Exception {
Object tree = findMethod("getTree", Element.class).invoke(getInstance(), element);
return (tree == null ? null : new Tree(tree));
}
public static Trees instance(ProcessingEnvironment env) throws Exception {
ClassLoader classLoader = env.getClass().getClassLoader();
Class<?> type = findClass(classLoader, "com.sun.source.util.Trees");
Method method = findMethod(type, "instance", ProcessingEnvironment.class);
return new Trees(method.invoke(null, env));
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.configurationprocessor.fieldvalues.javac;
import java.util.Collections;
import java.util.Set;
import javax.lang.model.element.Modifier;
/**
* Reflection based access to {@code com.sun.source.tree.VariableTree}.
*
* @author Phillip Webb
* @since 1.2.0
*/
class VariableTree extends ReflectionWrapper {
public VariableTree(Object instance) {
super(instance);
}
public String getName() throws Exception {
return findMethod("getName").invoke(getInstance()).toString();
}
public String getType() throws Exception {
return findMethod("getType").invoke(getInstance()).toString();
}
public ExpressionTree getInitializer() throws Exception {
Object instance = findMethod("getInitializer").invoke(getInstance());
return (instance == null ? null : new ExpressionTree(instance));
}
@SuppressWarnings("unchecked")
public Set<Modifier> getModifierFlags() throws Exception {
Object modifiers = findMethod("getModifiers").invoke(getInstance());
if (modifiers == null) {
return Collections.emptySet();
}
return (Set<Modifier>) findMethod(findClass("com.sun.source.tree.ModifiersTree"),
"getFlags").invoke(modifiers);
}
}

View File

@@ -38,8 +38,11 @@ public class ItemMetadata implements Comparable<ItemMetadata> {
private String sourceMethod;
private Object defaultValue;
ItemMetadata(ItemType itemType, String prefix, String name, String type,
String sourceType, String sourceMethod, String description) {
String sourceType, String sourceMethod, String description,
Object defaultValue) {
super();
this.itemType = itemType;
this.name = buildName(prefix, name);
@@ -47,6 +50,7 @@ public class ItemMetadata implements Comparable<ItemMetadata> {
this.sourceType = sourceType;
this.sourceMethod = sourceMethod;
this.description = description;
this.defaultValue = defaultValue;
}
private String buildName(String prefix, String name) {
@@ -85,12 +89,17 @@ public class ItemMetadata implements Comparable<ItemMetadata> {
return this.description;
}
public Object getDefaultValue() {
return this.defaultValue;
}
@Override
public String toString() {
StringBuilder string = new StringBuilder(this.name);
buildToStringProperty(string, "type", this.type);
buildToStringProperty(string, "sourceType", this.sourceType);
buildToStringProperty(string, "description", this.description);
buildToStringProperty(string, "defaultValue", this.defaultValue);
return string.toString();
}
@@ -109,13 +118,14 @@ public class ItemMetadata implements Comparable<ItemMetadata> {
public static ItemMetadata newGroup(String name, String type, String sourceType,
String sourceMethod) {
return new ItemMetadata(ItemType.GROUP, name, null, type, sourceType,
sourceMethod, null);
sourceMethod, null, null);
}
public static ItemMetadata newProperty(String prefix, String name, String type,
String sourceType, String sourceMethod, String description) {
String sourceType, String sourceMethod, String description,
Object defaultValue) {
return new ItemMetadata(ItemType.PROPERTY, prefix, name, type, sourceType,
sourceMethod, description);
sourceMethod, description, defaultValue);
}
/**

View File

@@ -67,6 +67,7 @@ public class JsonMarshaller {
putIfPresent(jsonObject, "description", item.getDescription());
putIfPresent(jsonObject, "sourceType", item.getSourceType());
putIfPresent(jsonObject, "sourceMethod", item.getSourceMethod());
putIfPresent(jsonObject, "defaultValue", item.getDefaultValue());
return jsonObject;
}
@@ -101,8 +102,9 @@ public class JsonMarshaller {
String description = object.optString("description", null);
String sourceType = object.optString("sourceType", null);
String sourceMethod = object.optString("sourceMethod", null);
Object defaultValue = object.opt("defaultValue");
return new ItemMetadata(itemType, name, null, type, sourceType, sourceMethod,
description);
description, defaultValue);
}
private String toString(InputStream inputStream) throws IOException {