Add support for property hint

Create a new section in the meta-data called "hints" where users can
provide hints about a given property. The most basic use case for now
is to provide a list of values that a property can have. Each value may
have a description.

This sample JSON provides a basic example for a property called `foo.mode`
that exposes 3 values: "auto", "basic" and "advanced".

```
 "hints": [
    {
      "id": "foo.mode",
      "values": [
        {
          "value": "auto",
          "description": "Some smart description."
        },
        {
          "name": "basic"
        },
        {
          "name": "advanced"
        }
      ]
    }
]
```

This information can be read by tools (such as IDE) and offer an
auto-completion with the list of values.

Closes gh-2054
This commit is contained in:
Stephane Nicoll
2015-06-23 11:37:49 +02:00
parent 9aa445bf46
commit bc9321734f
7 changed files with 505 additions and 27 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* 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.
@@ -36,12 +36,16 @@ public class ConfigurationMetadata {
private final List<ItemMetadata> items;
private final List<ItemHint> hints;
public ConfigurationMetadata() {
this.items = new ArrayList<ItemMetadata>();
this.hints = new ArrayList<ItemHint>();
}
public ConfigurationMetadata(ConfigurationMetadata metadata) {
this.items = new ArrayList<ItemMetadata>(metadata.getItems());
this.hints = new ArrayList<ItemHint>(metadata.getHints());
}
/**
@@ -53,6 +57,11 @@ public class ConfigurationMetadata {
Collections.sort(this.items);
}
public void add(ItemHint itemHint) {
this.hints.add(itemHint);
Collections.sort(this.hints);
}
/**
* Add all properties from another {@link ConfigurationMetadata}.
* @param metadata the {@link ConfigurationMetadata} instance to merge
@@ -60,6 +69,8 @@ public class ConfigurationMetadata {
public void addAll(ConfigurationMetadata metadata) {
this.items.addAll(metadata.getItems());
Collections.sort(this.items);
this.hints.addAll(metadata.getHints());
Collections.sort(this.hints);
}
/**
@@ -69,6 +80,13 @@ public class ConfigurationMetadata {
return Collections.unmodifiableList(this.items);
}
/**
* @return the meta-data hints.
*/
public List<ItemHint> getHints() {
return Collections.unmodifiableList(this.hints);
}
public static String nestedPrefix(String prefix, String name) {
String nestedPrefix = (prefix == null ? "" : prefix);
String dashedName = toDashedCase(name);

View File

@@ -0,0 +1,107 @@
/*
* 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.metadata;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Provide hints on an {@link ItemMetadata}. Defines the list of possible values for
* a particular item as {@link ItemHint.ValueHint} instances.
* <p>
* The {@code name} of the hint is the name of the related property with one major
* exception for map types as both the keys and values of the map can have hints. In
* such a case, the hint should be suffixed by ".key" or ".values" respectively. Creating
* a hint for a map using its property name is therefore invalid.
*
* @author Stephane Nicoll
* @since 1.3.0
*/
public class ItemHint implements Comparable<ItemHint> {
private final String name;
private final List<ValueHint> values;
public ItemHint(String name, List<ValueHint> values) {
this.name = toCanonicalName(name);
this.values = new ArrayList<ValueHint>(values);
}
private String toCanonicalName(String name) {
int dot = name.lastIndexOf('.');
if (dot != -1) {
String prefix = name.substring(0, dot);
String originalName = name.substring(dot, name.length());
return prefix + ConfigurationMetadata.toDashedCase(originalName);
}
return ConfigurationMetadata.toDashedCase(name);
}
public String getName() {
return this.name;
}
public List<ValueHint> getValues() {
return Collections.unmodifiableList(this.values);
}
@Override
public int compareTo(ItemHint other) {
return getName().compareTo(other.getName());
}
public static ItemHint newHint(String name, ValueHint... values) {
return new ItemHint(name, Arrays.asList(values));
}
@Override
public String toString() {
return "ItemHint{" + "name='" + this.name + '\'' +
", values=" + this.values +
'}';
}
public static class ValueHint {
private final Object value;
private final String description;
public ValueHint(Object value, String description) {
this.value = value;
this.description = description;
}
public Object getValue() {
return this.value;
}
public String getDescription() {
return this.description;
}
@Override
public String toString() {
return "ValueHint{" + "value=" + this.value +
", description='" + this.description + '\'' +
'}';
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* 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.
@@ -22,7 +22,10 @@ import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.json.JSONArray;
@@ -48,6 +51,7 @@ public class JsonMarshaller {
JSONObject object = new JSONObject();
object.put("groups", toJsonArray(metadata, ItemType.GROUP));
object.put("properties", toJsonArray(metadata, ItemType.PROPERTY));
object.put("hints", toJsonArray(metadata.getHints()));
outputStream.write(object.toString(2).getBytes(UTF_8));
}
@@ -61,6 +65,14 @@ public class JsonMarshaller {
return jsonArray;
}
private JSONArray toJsonArray(Collection<ItemHint> hints) {
JSONArray jsonArray = new JSONArray();
for (ItemHint hint : hints) {
jsonArray.put(toJsonObject(hint));
}
return jsonArray;
}
private JSONObject toJsonObject(ItemMetadata item) {
JSONObject jsonObject = new JSONOrderedObject();
jsonObject.put("name", item.getName());
@@ -78,13 +90,40 @@ public class JsonMarshaller {
return jsonObject;
}
private JSONObject toJsonObject(ItemHint hint) {
JSONObject jsonObject = new JSONOrderedObject();
jsonObject.put("name", hint.getName());
if (!hint.getValues().isEmpty()) {
JSONArray valuesArray = new JSONArray();
for (ItemHint.ValueHint valueHint : hint.getValues()) {
JSONObject valueObject = new JSONOrderedObject();
putHintValue(valueObject, valueHint.getValue());
putIfPresent(valueObject, "description", valueHint.getDescription());
valuesArray.put(valueObject);
}
jsonObject.put("values", valuesArray);
}
return jsonObject;
}
private void putIfPresent(JSONObject jsonObject, String name, Object value) {
if (value != null) {
jsonObject.put(name, value);
}
}
private void putHintValue(JSONObject jsonObject, Object value) {
Object hintValue = extractItemValue(value);
jsonObject.put("value", hintValue);
}
private void putDefaultValue(JSONObject jsonObject, Object value) {
Object defaultValue = extractItemValue(value);
jsonObject.put("defaultValue", defaultValue);
}
private Object extractItemValue(Object value) {
Object defaultValue = value;
if (value.getClass().isArray()) {
JSONArray array = new JSONArray();
@@ -95,7 +134,7 @@ public class JsonMarshaller {
defaultValue = array;
}
jsonObject.put("defaultValue", defaultValue);
return defaultValue;
}
public ConfigurationMetadata read(InputStream inputStream) throws IOException {
@@ -114,6 +153,12 @@ public class JsonMarshaller {
ItemType.PROPERTY));
}
}
JSONArray hints = object.optJSONArray("hints");
if (hints != null) {
for (int i = 0; i < hints.length(); i++) {
metadata.add(toItemHint((JSONObject) hints.get(i)));
}
}
return metadata;
}
@@ -123,23 +168,41 @@ public class JsonMarshaller {
String description = object.optString("description", null);
String sourceType = object.optString("sourceType", null);
String sourceMethod = object.optString("sourceMethod", null);
Object defaultValue = readDefaultValue(object);
Object defaultValue = readItemValue(object.opt("defaultValue"));
boolean deprecated = object.optBoolean("deprecated");
return new ItemMetadata(itemType, name, null, type, sourceType, sourceMethod,
description, defaultValue, deprecated);
}
private Object readDefaultValue(JSONObject object) {
Object defaultValue = object.opt("defaultValue");
if (defaultValue instanceof JSONArray) {
JSONArray array = (JSONArray) defaultValue;
private ItemHint toItemHint(JSONObject object) {
String name = object.getString("name");
List<ItemHint.ValueHint> values = new ArrayList<ItemHint.ValueHint>();
if (object.has("values")) {
JSONArray valuesArray = object.getJSONArray("values");
for (int i = 0; i < valuesArray.length(); i++) {
values.add(toValueHint((JSONObject) valuesArray.get(i)));
}
}
return new ItemHint(name, values);
}
private ItemHint.ValueHint toValueHint(JSONObject object) {
Object value = readItemValue(object.get("value"));
String description = object.optString("description", null);
return new ItemHint.ValueHint(value, description);
}
private Object readItemValue(Object value) {
if (value instanceof JSONArray) {
JSONArray array = (JSONArray) value;
Object[] content = new Object[array.length()];
for (int i = 0; i < array.length(); i++) {
content[i] = array.get(i);
}
return content;
}
return defaultValue;
return value;
}
private String toString(InputStream inputStream) throws IOException {

View File

@@ -27,6 +27,7 @@ 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.ItemHint;
import org.springframework.boot.configurationsample.incremental.BarProperties;
import org.springframework.boot.configurationsample.incremental.FooProperties;
import org.springframework.boot.configurationsample.incremental.RenamedBarProperties;
@@ -58,6 +59,7 @@ 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.containsHint;
import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsProperty;
import static org.springframework.boot.configurationprocessor.MetadataStore.METADATA_PATH;
@@ -324,12 +326,8 @@ public class ConfigurationMetadataAnnotationProcessorTests {
}
@Test
public void mergingOfAdditionalMetadata() throws Exception {
File metaInfFolder = new File(this.compiler.getOutputLocation(), "META-INF");
metaInfFolder.mkdirs();
File additionalMetadataFile = new File(metaInfFolder,
"additional-spring-configuration-metadata.json");
additionalMetadataFile.createNewFile();
public void mergingOfAdditionalProperty() throws Exception {
File additionalMetadataFile = createAdditionalMetadataFile();
JSONObject property = new JSONObject();
property.put("name", "foo");
@@ -339,10 +337,8 @@ public class ConfigurationMetadataAnnotationProcessorTests {
properties.put(property);
JSONObject additionalMetadata = new JSONObject();
additionalMetadata.put("properties", properties);
FileWriter writer = new FileWriter(additionalMetadataFile);
additionalMetadata.write(writer);
writer.flush();
writeMetadata(additionalMetadataFile, additionalMetadata);
ConfigurationMetadata metadata = compile(SimpleProperties.class);
assertThat(metadata, containsProperty("simple.comparator"));
@@ -352,6 +348,28 @@ public class ConfigurationMetadataAnnotationProcessorTests {
.fromSource(AdditionalMetadata.class));
}
@Test
public void mergingOfSimpleHint() throws Exception {
writeAdditionalHints(
ItemHint.newHint("simple.the-name", new ItemHint.ValueHint("boot", "Bla bla"),
new ItemHint.ValueHint("spring", null)));
ConfigurationMetadata metadata = compile(SimpleProperties.class);
assertThat(metadata, containsHint("simple.the-name")
.withValue(0, "boot", "Bla bla")
.withValue(1, "spring", null));
}
@Test
public void mergingOfHintWithNonCanonicalName() throws Exception {
writeAdditionalHints(
ItemHint.newHint("simple.theName", new ItemHint.ValueHint("boot", "Bla bla")));
ConfigurationMetadata metadata = compile(SimpleProperties.class);
assertThat(metadata, containsHint("simple.the-name")
.withValue(0, "boot", "Bla bla"));
}
@Test
public void incrementalBuild() throws Exception {
TestProject project = new TestProject(this.temporaryFolder, FooProperties.class,
@@ -448,6 +466,50 @@ public class ConfigurationMetadataAnnotationProcessorTests {
return processor.getMetadata();
}
private void writeAdditionalHints(ItemHint... hints) throws IOException {
File additionalMetadataFile = createAdditionalMetadataFile();
JSONArray hintsArray = new JSONArray();
for (ItemHint hint : hints) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", hint.getName());
JSONArray valuesArray = new JSONArray();
for (ItemHint.ValueHint valueHint : hint.getValues()) {
JSONObject valueJsonObject = new JSONObject();
valueJsonObject.put("value", valueHint.getValue());
String description = valueHint.getDescription();
if (description != null) {
valueJsonObject.put("description", description);
}
valuesArray.put(valueJsonObject);
}
jsonObject.put("values", valuesArray);
hintsArray.put(jsonObject);
}
JSONObject additionalMetadata = new JSONObject();
additionalMetadata.put("hints", hints);
writeMetadata(additionalMetadataFile, additionalMetadata);
}
private File createAdditionalMetadataFile() throws IOException {
File metaInfFolder = new File(this.compiler.getOutputLocation(), "META-INF");
metaInfFolder.mkdirs();
File additionalMetadataFile = new File(metaInfFolder,
"additional-spring-configuration-metadata.json");
additionalMetadataFile.createNewFile();
return additionalMetadataFile;
}
private void writeMetadata(File metadataFile, JSONObject metadata) throws IOException {
FileWriter writer = new FileWriter(metadataFile);
try {
metadata.write(writer);
}
finally {
writer.close();
}
}
private static class AdditionalMetadata {
}

View File

@@ -16,10 +16,14 @@
package org.springframework.boot.configurationprocessor;
import java.util.ArrayList;
import java.util.List;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemHint;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata.ItemType;
@@ -27,6 +31,7 @@ import org.springframework.boot.configurationprocessor.metadata.ItemMetadata.Ite
* Hamcrest {@link Matcher} to help test {@link ConfigurationMetadata}.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
public class ConfigurationMetadataMatchers {
@@ -54,6 +59,10 @@ public class ConfigurationMetadataMatchers {
return new ContainsItemMatcher(ItemType.PROPERTY, name).ofType(type);
}
public static ContainsHintMatcher containsHint(String name) {
return new ContainsHintMatcher(name);
}
public static class ContainsItemMatcher extends BaseMatcher<ConfigurationMetadata> {
private final ItemType itemType;
@@ -89,7 +98,7 @@ public class ConfigurationMetadataMatchers {
@Override
public boolean matches(Object item) {
ConfigurationMetadata metadata = (ConfigurationMetadata) item;
ItemMetadata itemMetadata = getFirstPropertyWithName(metadata, this.name);
ItemMetadata itemMetadata = getFirstItemWithName(metadata, this.name);
if (itemMetadata == null) {
return false;
}
@@ -117,7 +126,7 @@ public class ConfigurationMetadataMatchers {
@Override
public void describeMismatch(Object item, Description description) {
ConfigurationMetadata metadata = (ConfigurationMetadata) item;
ItemMetadata property = getFirstPropertyWithName(metadata, this.name);
ItemMetadata property = getFirstItemWithName(metadata, this.name);
if (property == null) {
description.appendText("missing "
+ this.itemType.toString().toLowerCase() + " " + this.name);
@@ -179,7 +188,7 @@ public class ConfigurationMetadataMatchers {
this.sourceType, this.description, this.defaultValue, true);
}
private ItemMetadata getFirstPropertyWithName(ConfigurationMetadata metadata,
private ItemMetadata getFirstItemWithName(ConfigurationMetadata metadata,
String name) {
for (ItemMetadata item : metadata.getItems()) {
if (item.isOfItemType(this.itemType) && name.equals(item.getName())) {
@@ -191,4 +200,127 @@ public class ConfigurationMetadataMatchers {
}
public static class ContainsHintMatcher extends BaseMatcher<ConfigurationMetadata> {
private final String name;
private final List<ValueHintMatcher> values;
public ContainsHintMatcher(String name) {
this(name, new ArrayList<ValueHintMatcher>());
}
public ContainsHintMatcher(String name, List<ValueHintMatcher> values) {
this.name = name;
this.values = values;
}
@Override
public boolean matches(Object item) {
ConfigurationMetadata metadata = (ConfigurationMetadata) item;
ItemHint itemHint = getFirstHintWithName(metadata, this.name);
if (itemHint == null) {
return false;
}
if (this.name != null && !this.name.equals(itemHint.getName())) {
return false;
}
for (ValueHintMatcher value : this.values) {
if (!value.matches(itemHint)) {
return false;
}
}
return true;
}
@Override
public void describeMismatch(Object item, Description description) {
ConfigurationMetadata metadata = (ConfigurationMetadata) item;
ItemHint itemHint = getFirstHintWithName(metadata, this.name);
if (itemHint == null) {
description.appendText("missing hint " + this.name);
}
else {
description.appendText(
"was hint ").appendValue(itemHint);
}
}
@Override
public void describeTo(Description description) {
description.appendText("hints for " + this.name);
if (this.values != null) {
description.appendText(" values ").appendValue(this.values);
}
}
public ContainsHintMatcher withValue(int index, Object value, String description) {
List<ValueHintMatcher> values = new ArrayList<ValueHintMatcher>(this.values);
values.add(new ValueHintMatcher(index, value, description));
return new ContainsHintMatcher(this.name, values);
}
private ItemHint getFirstHintWithName(ConfigurationMetadata metadata,
String name) {
for (ItemHint hint : metadata.getHints()) {
if (name.equals(hint.getName())) {
return hint;
}
}
return null;
}
}
public static class ValueHintMatcher extends BaseMatcher<ItemHint> {
private final int index;
private final Object value;
private final String description;
public ValueHintMatcher(int index, Object value, String description) {
this.index = index;
this.value = value;
this.description = description;
}
@Override
public boolean matches(Object item) {
ItemHint hint = (ItemHint) item;
if (this.index + 1 > hint.getValues().size()) {
return false;
}
ItemHint.ValueHint valueHint = hint.getValues().get(this.index);
if (this.value != null
&& !this.value.equals(valueHint.getValue())) {
return false;
}
if (this.description != null
&& !this.description.equals(valueHint.getDescription())) {
return false;
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText("value hint at index '"+this.index+"'");
if (this.value != null) {
description.appendText(" value ").appendValue(this.value);
}
if (this.description != null) {
description.appendText(" description ").appendValue(this.description);
}
}
private ItemHint.ValueHint getValueHint(ItemHint hint) {
for (ItemHint.ValueHint valueHint : hint.getValues()) {
if (this.value.equals(valueHint.getValue())) {
return valueHint;
}
}
return null;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2014 the original author or authors.
* 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.
@@ -26,6 +26,7 @@ import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsGroup;
import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsHint;
import static org.springframework.boot.configurationprocessor.ConfigurationMetadataMatchers.containsProperty;
/**
@@ -52,6 +53,9 @@ public class JsonMarshallerTests {
metadata.add(ItemMetadata.newProperty("f", null, null, null, null, null,
new Boolean[] { true, false }, false));
metadata.add(ItemMetadata.newGroup("d", null, null, null));
metadata.add(ItemHint.newHint("a.b"));
metadata.add(ItemHint.newHint("c", new ItemHint.ValueHint(123, "hey"),
new ItemHint.ValueHint(456, null)));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
JsonMarshaller marshaller = new JsonMarshaller();
marshaller.write(metadata, outputStream);
@@ -69,6 +73,8 @@ public class JsonMarshallerTests {
assertThat(read,
containsProperty("f").withDefaultValue(is(new boolean[] { true, false })));
assertThat(read, containsGroup("d"));
assertThat(read, containsHint("a.b"));
assertThat(read, containsHint("c").withValue(0, 123, "hey").withValue(1, 456, null));
}
}