Add support for value provider

Improve the "hints" section of the metadata so that each hint can provide
the reference to a value provider.

A value provider defines how a tool can discover the potential values of
a property based on the context. The provider is identifed by a name and
may have an arbitrary number of parameters.

Closes gh-3303
This commit is contained in:
Stephane Nicoll
2015-06-23 17:55:50 +02:00
parent 43b9ea53d6
commit 0ec9de9137
6 changed files with 511 additions and 12 deletions

View File

@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* Provide hints on an {@link ItemMetadata}. Defines the list of possible values for a
@@ -27,7 +28,7 @@ import java.util.List;
* <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
* a case, the hint should be suffixed by ".keys" or ".values" respectively. Creating a
* hint for a map using its property name is therefore invalid.
*
* @author Stephane Nicoll
@@ -39,9 +40,12 @@ public class ItemHint implements Comparable<ItemHint> {
private final List<ValueHint> values;
public ItemHint(String name, List<ValueHint> values) {
private final List<ProviderHint> providers;
public ItemHint(String name, List<ValueHint> values, List<ProviderHint> providers) {
this.name = toCanonicalName(name);
this.values = new ArrayList<ValueHint>(values);
this.values = (values != null ? new ArrayList<ValueHint>(values) : new ArrayList<ValueHint>());
this.providers = (providers != null ? new ArrayList<ProviderHint>(providers) : new ArrayList<ProviderHint>());
}
private String toCanonicalName(String name) {
@@ -62,19 +66,23 @@ public class ItemHint implements Comparable<ItemHint> {
return Collections.unmodifiableList(this.values);
}
public List<ProviderHint> getProviders() {
return Collections.unmodifiableList(this.providers);
}
@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));
return new ItemHint(name, Arrays.asList(values), Collections.<ProviderHint>emptyList());
}
@Override
public String toString() {
return "ItemHint{" + "name='" + this.name + '\'' + ", values=" + this.values
+ '}';
return "ItemHint{" + "name='" + this.name + ", values=" + this.values
+ "providers=" + this.providers + '}';
}
public static class ValueHint {
@@ -104,4 +112,28 @@ public class ItemHint implements Comparable<ItemHint> {
}
public static class ProviderHint {
private final String name;
private final Map<String,Object> parameters;
public ProviderHint(String name, Map<String, Object> parameters) {
this.name = name;
this.parameters = parameters;
}
public String getName() {
return name;
}
public Map<String, Object> getParameters() {
return parameters;
}
@Override
public String toString() {
return "Provider{" + "name='" + this.name + ", parameters=" + this.parameters +
'}';
}
}
}

View File

@@ -24,8 +24,10 @@ import java.lang.reflect.Array;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.JSONArray;
@@ -103,6 +105,22 @@ public class JsonMarshaller {
}
jsonObject.put("values", valuesArray);
}
if (!hint.getProviders().isEmpty()) {
JSONArray providersArray = new JSONArray();
for (ItemHint.ProviderHint providerHint : hint.getProviders()) {
JSONObject providerHintObject = new JSONOrderedObject();
providerHintObject.put("name", providerHint.getName());
if (providerHint.getParameters() != null && !providerHint.getParameters().isEmpty()) {
JSONObject parametersObject = new JSONOrderedObject();
for (Map.Entry<String, Object> entry : providerHint.getParameters().entrySet()) {
parametersObject.put(entry.getKey(), extractItemValue(entry.getValue()));
}
providerHintObject.put("parameters", parametersObject);
}
providersArray.put(providerHintObject);
}
jsonObject.put("providers", providersArray);
}
return jsonObject;
}
@@ -182,7 +200,14 @@ public class JsonMarshaller {
values.add(toValueHint((JSONObject) valuesArray.get(i)));
}
}
return new ItemHint(name, values);
List<ItemHint.ProviderHint> providers = new ArrayList<ItemHint.ProviderHint>();
if (object.has("providers")) {
JSONArray providersObject = object.getJSONArray("providers");
for (int i = 0; i < providersObject.length(); i++) {
providers.add(toProviderHint((JSONObject) providersObject.get(i)));
}
}
return new ItemHint(name, values, providers);
}
private ItemHint.ValueHint toValueHint(JSONObject object) {
@@ -191,6 +216,20 @@ public class JsonMarshaller {
return new ItemHint.ValueHint(value, description);
}
private ItemHint.ProviderHint toProviderHint(JSONObject object) {
String name = object.getString("name");
Map<String,Object> parameters = new HashMap<String,Object>();
if (object.has("parameters")) {
JSONObject parametersObject = object.getJSONObject("parameters");
for (Object k : parametersObject.keySet()) {
String key = (String) k;
Object value = readItemValue(parametersObject.get(key));
parameters.put(key, value);
}
}
return new ItemHint.ProviderHint(name, parameters);
}
private Object readItemValue(Object value) {
if (value instanceof JSONArray) {
JSONArray array = (JSONArray) value;

View File

@@ -19,6 +19,8 @@ package org.springframework.boot.configurationprocessor;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -369,6 +371,20 @@ public class ConfigurationMetadataAnnotationProcessorTests {
containsHint("simple.the-name").withValue(0, "boot", "Bla bla"));
}
@Test
public void mergingOfHintWithProvider() throws Exception {
writeAdditionalHints(
new ItemHint("simple.theName", Collections.<ItemHint.ValueHint>emptyList(), Arrays.asList(
new ItemHint.ProviderHint("first", Collections.<String,Object>singletonMap("target", "org.foo")),
new ItemHint.ProviderHint("second", null))
));
ConfigurationMetadata metadata = compile(SimpleProperties.class);
assertThat(metadata, containsHint("simple.the-name")
.withProvider("first", "target", "org.foo")
.withProvider("second"));
}
@Test
public void incrementalBuild() throws Exception {
TestProject project = new TestProject(this.temporaryFolder, FooProperties.class,

View File

@@ -17,11 +17,14 @@
package org.springframework.boot.configurationprocessor;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.collection.IsMapContaining;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemHint;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
@@ -206,13 +209,16 @@ public class ConfigurationMetadataMatchers {
private final List<ValueHintMatcher> values;
private final List<ProviderHintMatcher> providers;
public ContainsHintMatcher(String name) {
this(name, new ArrayList<ValueHintMatcher>());
this(name, new ArrayList<ValueHintMatcher>(), new ArrayList<ProviderHintMatcher>());
}
public ContainsHintMatcher(String name, List<ValueHintMatcher> values) {
public ContainsHintMatcher(String name, List<ValueHintMatcher> values, List<ProviderHintMatcher> providers) {
this.name = name;
this.values = values;
this.providers = providers;
}
@Override
@@ -230,6 +236,11 @@ public class ConfigurationMetadataMatchers {
return false;
}
}
for (ProviderHintMatcher provider : this.providers) {
if (!provider.matches(itemHint)) {
return false;
}
}
return true;
}
@@ -251,12 +262,29 @@ public class ConfigurationMetadataMatchers {
if (this.values != null) {
description.appendText(" values ").appendValue(this.values);
}
if (this.providers != null) {
description.appendText(" providers ").appendValue(this.providers);
}
}
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);
return new ContainsHintMatcher(this.name, values, this.providers);
}
public ContainsHintMatcher withProvider(int index, String provider, Map<String,Object> parameters) {
List<ProviderHintMatcher> providers = new ArrayList<ProviderHintMatcher>(this.providers);
providers.add(new ProviderHintMatcher(index, provider, parameters));
return new ContainsHintMatcher(this.name, this.values, providers);
}
public ContainsHintMatcher withProvider(String provider, String key, Object value) {
return withProvider(this.providers.size(), provider, Collections.singletonMap(key, value));
}
public ContainsHintMatcher withProvider(String provider) {
return withProvider(this.providers.size(), provider, null);
}
private ItemHint getFirstHintWithName(ConfigurationMetadata metadata, String name) {
@@ -314,4 +342,50 @@ public class ConfigurationMetadataMatchers {
}
public static class ProviderHintMatcher extends BaseMatcher<ItemHint> {
private final int index;
private final String name;
private final Map<String, Object> parameters;
public ProviderHintMatcher(int index, String name, Map<String, Object> parameters) {
this.index = index;
this.name = name;
this.parameters = parameters;
}
@Override
public boolean matches(Object item) {
ItemHint hint = (ItemHint) item;
if (this.index + 1 > hint.getProviders().size()) {
return false;
}
ItemHint.ProviderHint providerHint = hint.getProviders().get(index);
if (this.name != null
&& !this.name.equals(providerHint.getName())) {
return false;
}
if (this.parameters != null) {
for (Map.Entry<String, Object> entry : this.parameters.entrySet()) {
if (!IsMapContaining.hasEntry(entry.getKey(), entry.getValue())
.matches(providerHint.getParameters())) {
return false;
}
}
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendText("provider hint ");
if (this.name != null) {
description.appendText(" name ").appendValue(this.name);
}
if (this.parameters != null) {
description.appendText(" parameters ").appendValue(this.parameters);
}
}
}
}

View File

@@ -20,6 +20,8 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
@@ -56,6 +58,9 @@ public class JsonMarshallerTests {
metadata.add(ItemHint.newHint("a.b"));
metadata.add(ItemHint.newHint("c", new ItemHint.ValueHint(123, "hey"),
new ItemHint.ValueHint(456, null)));
metadata.add(new ItemHint("d", null, Arrays.asList(
new ItemHint.ProviderHint("first", Collections.<String,Object>singletonMap("target", "foo")),
new ItemHint.ProviderHint("second", null))));
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
JsonMarshaller marshaller = new JsonMarshaller();
marshaller.write(metadata, outputStream);
@@ -76,6 +81,9 @@ public class JsonMarshallerTests {
assertThat(read, containsHint("a.b"));
assertThat(read,
containsHint("c").withValue(0, 123, "hey").withValue(1, 456, null));
assertThat(read, containsHint("d")
.withProvider("first", "target", "foo")
.withProvider("second"));
}
}