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
pull/3333/head
Stephane Nicoll 10 years ago
parent 43b9ea53d6
commit 0ec9de9137

@ -71,6 +71,11 @@ categorized under "hints":
"value": "force", "value": "force",
"description": "Enable compression of all responses." "description": "Enable compression of all responses."
}, },
],
"providers": [
{
"name": "any"
}
] ]
} }
]} ]}
@ -210,6 +215,12 @@ The JSON object contained in the `hints` array can contain the following attribu
| ValueHint[] | ValueHint[]
| A list of valid values as defined by the `ValueHint` object (see below). Each entry defines | A list of valid values as defined by the `ValueHint` object (see below). Each entry defines
the value and may have a description the value and may have a description
|`providers`
| ProviderHint[]
| A list of providers as defined by the `ValueHint` object (see below). Each entry defines
the name of the provider and its parameters, if any.
|=== |===
The JSON object contained in the `values` array of each `hint` element can contain the The JSON object contained in the `values` array of each `hint` element can contain the
@ -232,6 +243,24 @@ following attributes:
end with a period (`.`). end with a period (`.`).
|=== |===
The JSON object contained in the `providers` array of each `hint` element can contain the
following attributes:
[cols="1,1,4"]
|===
|Name | Type |Purpose
|`name`
| String
| The name of the provider to use to offer additional content assistance for the element
to which the hint refers to.
|`parameters`
| JSON object
| Any additional parameter that the provider supports (check the documentation of the
provider for more details).
|===
[[configuration-metadata-repeated-items]] [[configuration-metadata-repeated-items]]
==== Repeated meta-data items ==== Repeated meta-data items
It is perfectly acceptable for "`property`" and "`group`" objects with the same name to It is perfectly acceptable for "`property`" and "`group`" objects with the same name to
@ -245,8 +274,14 @@ that they support such scenarios.
[[configuration-metadata-providing-manual-hints]] [[configuration-metadata-providing-manual-hints]]
=== Providing manual hints === Providing manual hints
To improve the user experience and further assist the user in configuring a given To improve the user experience and further assist the user in configuring a given
property, you can provide additional meta-data that describes the list of potential property, you can provide additional meta-data that:
values for a property.
1. Describes the list of potential values for a property.
2. Associates a provider to attach a well-defined semantic to a property so that a tool
can discover the list of potential values based on the project's context.
==== Value hints
The `name` attribute of each hint refers to the `name` of a property. In the initial The `name` attribute of each hint refers to the `name` of a property. In the initial
example above, we provide 3 values for the `server.tomcat.compression` property: `on`, example above, we provide 3 values for the `server.tomcat.compression` property: `on`,
@ -256,7 +291,302 @@ If your property is of type `Map`, you can provide hints for both the keys and t
values (but not for the map itself). The special `.keys` and `.values` suffixes must values (but not for the map itself). The special `.keys` and `.values` suffixes must
be used to refer to the keys and the values respectively. be used to refer to the keys and the values respectively.
Let's assume a `foo.contexts` that maps magic String values to an integer:
[source,java,indent=0]
----
@ConfigurationProperties("foo")
public class FooProperties {
private Map<String,Integer> contexts;
// getters and setters
}
----
The magic values are foo and bar for instance. In order to offer additional content
assistance for the keys, you could add the following to
<<configuration-metadata-additional-metadata,the manual meta-data of the module>>:
[source,json,indent=0]
----
{"hints": [
{
"name": "foo.contexts.keys",
"values": [
{
"value": "foo"
},
{
"value": "bar"
}
]
}
]}
----
NOTE: Of course, you should have an `Enum` for those two values instead. This is by far
the most effective approach to auto-completion if your IDE supports it.
==== Provider hints
Providers are a powerful way of attaching semantics to a property. We define in the section
below the official providers that you can use for your own hints. Bare in mind however that
your favorite IDE may implement some of these or none of them. It could eventually provide
its own as well.
NOTE: As this is a new feature, IDE vendors will have to catch up with this new feature.
The table below summarizes the list of supported providers:
[cols="2,4"]
|===
|Name | Description
|`any`
|Permit any additional values to be provided.
|`class-reference`
|Auto-complete the classes available in the project. Usually constrained by a base
class that is specified via the `target` parameter.
|`enum`
|Auto-complete the values of an enum given by the mandatory `target` parameter.
|`logger-name`
|Auto-complete valid logger names. Typically, package and class names available in
the current project can be auto-completed.
|`spring-bean-reference`
|Auto-complete the available bean names in the current project. Usually constrained
by a base class that is specified via the `target` parameter.
|===
TIP: No more than one provider can be active for a given property but you can specify
several providers if they can all manage the property _in some ways_. Make sure to place
the most powerful provider first as the IDE must use the first one in the JSON section it
can handle. If no provider for a given property is supported, no special content
assistance is provided either.
===== Any
The **any** provider permits any additional values to be provided. Regular value
validation based on the property type should be applied if this is supported.
This provider will be typically used if you have a list of values and any extra values
are still to be considered as valid.
The example below offers `on` and `off` as auto-completion values for `system.state`; any
other value is also allowed:
[source,json,indent=0]
----
{"hints": [
{
"name": "system.state",
"values": [
{
"value": "on"
},
{
"value": "off"
}
],
"providers": [
{
"name": "any"
}
]
}
]}
----
===== Class reference
The **class-reference** provider auto-completes classes available in the project. This
provider supports these parameters:
[cols="1,1,2,4"]
|===
|Parameter |Type |Default value |Description
|`target`
|`String` (`Class`)
|_none_
|The fully qualified name of the class that should be assignable to the chosen value.
Typically used to filter out non candidate classes. Note that this information can
be provided by the type itself by exposing a class with the appropriate upper bound.
|`concrete`
|`boolean`
|true
|Specify if only concrete classes are to be considered as valid candidates.
|===
The meta-data snippet below corresponds to the standard `server.jsp-servlet.class-name`
property that defines the `JspServlet` class name to use:
[source,json,indent=0]
----
{"hints": [
{
"name": "server.jsp-servlet.class-name",
"providers": [
{
"name": "class-reference",
"parameters": {
"target": "javax.servlet.http.HttpServlet"
}
}
]
}
]}
----
===== Enum
The **enum** provider auto-completes the values of the `Enum` class referenced via the
`target` parameter. This provider supports these parameters:
[cols="1,1,2,4"]
|===
|Parameter |Type |Default value |Description
| **`target`**
| `String` (`Enum`)
|_none_
|The fully qualified name of the `Enum` class. This parameter is mandatory.
|===
The meta-data snippet below corresponds to the standard `spring.jooq.sql-dialect`
property that defines the `SQLDialect` class name to use:
[source,json,indent=0]
----
{"hints": [
{
"name": "spring.jooq.sql-dialect",
"providers": [
{
"name": "enum",
"parameters": {
"target": "org.jooq.SQLDialect"
}
}
]
},
]}
----
TIP: This is useful when you don't want your configuration classes to rely on classes
that may not be on the classpath.
===== Logger name
The **logger-name** provider auto-completes valid logger names. Typically, package and
class names available in the current project can be auto-completed. Specific frameworks
may have extra magic logger names that could be supported as well.
Since a logger name can be any arbitrary name, really, this provider should allow any
value but could highlight valid packages and class names that are not available in the
project's classpath.
The meta-data snippet below corresponds to the standard `logger.level` property, keys
are _logger names_ and values correspond to the the standard log levels or any custom
level:
[source,json,indent=0]
----
{"hints": [
{
"name": "logger.level.keys",
"values": [
{
"value": "root",
"description": "Root logger used to assign the default logging level."
}
],
"providers": [
{
"name": "logger-name"
}
]
},
{
"name": "logger.level.values",
"values": [
{
"value": "trace"
},
{
"value": "debug"
},
{
"value": "info"
},
{
"value": "warn"
},
{
"value": "error"
},
{
"value": "fatal"
},
{
"value": "off"
}
],
"providers": [
{
"name": "any"
}
]
}
]}
----
===== Spring bean reference
The **spring-bean-reference** provider auto-completes the beans that are defined in
the configuration of the current project. This provider supports these parameters:
[cols="1,1,2,4"]
|===
|Parameter |Type |Default value |Description
|`target`
| `String` (`Class`)
|_none_
|The fully qualified name of the bean class that should be assignable to the candidate.
Typically used to filter out non candidate beans.
|===
The meta-data snippet below corresponds to the standard `spring.jmx.server` property
that defines the name of the `MBeanServer` bean to use:
[source,json,indent=0]
----
{"hints": [
{
"name": "spring.jmx.server",
"providers": [
{
"name": "spring-bean-reference",
"parameters": {
"target": "javax.management.MBeanServer"
}
}
]
}
]}
----
[[configuration-metadata-annotation-processor]] [[configuration-metadata-annotation-processor]]
=== Generating your own meta-data using the annotation processor === Generating your own meta-data using the annotation processor

@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* Provide hints on an {@link ItemMetadata}. Defines the list of possible values for a * Provide hints on an {@link ItemMetadata}. Defines the list of possible values for a
@ -27,7 +28,7 @@ import java.util.List;
* <p> * <p>
* The {@code name} of the hint is the name of the related property with one major * 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 * 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. * hint for a map using its property name is therefore invalid.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
@ -39,9 +40,12 @@ public class ItemHint implements Comparable<ItemHint> {
private final List<ValueHint> values; 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.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) { private String toCanonicalName(String name) {
@ -62,19 +66,23 @@ public class ItemHint implements Comparable<ItemHint> {
return Collections.unmodifiableList(this.values); return Collections.unmodifiableList(this.values);
} }
public List<ProviderHint> getProviders() {
return Collections.unmodifiableList(this.providers);
}
@Override @Override
public int compareTo(ItemHint other) { public int compareTo(ItemHint other) {
return getName().compareTo(other.getName()); return getName().compareTo(other.getName());
} }
public static ItemHint newHint(String name, ValueHint... values) { 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 @Override
public String toString() { 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 { 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 +
'}';
}
}
} }

@ -24,8 +24,10 @@ import java.lang.reflect.Array;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import org.json.JSONArray; import org.json.JSONArray;
@ -103,6 +105,22 @@ public class JsonMarshaller {
} }
jsonObject.put("values", valuesArray); 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; return jsonObject;
} }
@ -182,7 +200,14 @@ public class JsonMarshaller {
values.add(toValueHint((JSONObject) valuesArray.get(i))); 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) { private ItemHint.ValueHint toValueHint(JSONObject object) {
@ -191,6 +216,20 @@ public class JsonMarshaller {
return new ItemHint.ValueHint(value, description); 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) { private Object readItemValue(Object value) {
if (value instanceof JSONArray) { if (value instanceof JSONArray) {
JSONArray array = (JSONArray) value; JSONArray array = (JSONArray) value;

@ -19,6 +19,8 @@ package org.springframework.boot.configurationprocessor;
import java.io.File; import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
@ -369,6 +371,20 @@ public class ConfigurationMetadataAnnotationProcessorTests {
containsHint("simple.the-name").withValue(0, "boot", "Bla bla")); 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 @Test
public void incrementalBuild() throws Exception { public void incrementalBuild() throws Exception {
TestProject project = new TestProject(this.temporaryFolder, FooProperties.class, TestProject project = new TestProject(this.temporaryFolder, FooProperties.class,

@ -17,11 +17,14 @@
package org.springframework.boot.configurationprocessor; package org.springframework.boot.configurationprocessor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import org.hamcrest.BaseMatcher; import org.hamcrest.BaseMatcher;
import org.hamcrest.Description; import org.hamcrest.Description;
import org.hamcrest.Matcher; import org.hamcrest.Matcher;
import org.hamcrest.collection.IsMapContaining;
import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata;
import org.springframework.boot.configurationprocessor.metadata.ItemHint; import org.springframework.boot.configurationprocessor.metadata.ItemHint;
import org.springframework.boot.configurationprocessor.metadata.ItemMetadata; import org.springframework.boot.configurationprocessor.metadata.ItemMetadata;
@ -206,13 +209,16 @@ public class ConfigurationMetadataMatchers {
private final List<ValueHintMatcher> values; private final List<ValueHintMatcher> values;
private final List<ProviderHintMatcher> providers;
public ContainsHintMatcher(String name) { 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.name = name;
this.values = values; this.values = values;
this.providers = providers;
} }
@Override @Override
@ -230,6 +236,11 @@ public class ConfigurationMetadataMatchers {
return false; return false;
} }
} }
for (ProviderHintMatcher provider : this.providers) {
if (!provider.matches(itemHint)) {
return false;
}
}
return true; return true;
} }
@ -251,12 +262,29 @@ public class ConfigurationMetadataMatchers {
if (this.values != null) { if (this.values != null) {
description.appendText(" values ").appendValue(this.values); 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) { public ContainsHintMatcher withValue(int index, Object value, String description) {
List<ValueHintMatcher> values = new ArrayList<ValueHintMatcher>(this.values); List<ValueHintMatcher> values = new ArrayList<ValueHintMatcher>(this.values);
values.add(new ValueHintMatcher(index, value, description)); 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) { 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);
}
}
}
} }

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

Loading…
Cancel
Save