diff --git a/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc b/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc index c1b90d4fe5e..9a19c22007d 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc @@ -71,6 +71,11 @@ categorized under "hints": "value": "force", "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[] | A list of valid values as defined by the `ValueHint` object (see below). Each entry defines 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 @@ -232,6 +243,24 @@ following attributes: 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]] ==== Repeated meta-data items 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]] === Providing manual hints 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 -values for a property. +property, you can provide additional meta-data that: + +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 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 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 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 +<>: + +[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]] === Generating your own meta-data using the annotation processor diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemHint.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemHint.java index 954e7f45a6c..d8ef70c8893 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemHint.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemHint.java @@ -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; *

* 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 { private final List values; - public ItemHint(String name, List values) { + private final List providers; + + public ItemHint(String name, List values, List providers) { this.name = toCanonicalName(name); - this.values = new ArrayList(values); + this.values = (values != null ? new ArrayList(values) : new ArrayList()); + this.providers = (providers != null ? new ArrayList(providers) : new ArrayList()); } private String toCanonicalName(String name) { @@ -62,19 +66,23 @@ public class ItemHint implements Comparable { return Collections.unmodifiableList(this.values); } + public List 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.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 { } + public static class ProviderHint { + private final String name; + private final Map parameters; + + public ProviderHint(String name, Map parameters) { + this.name = name; + this.parameters = parameters; + } + + public String getName() { + return name; + } + + public Map getParameters() { + return parameters; + } + + @Override + public String toString() { + return "Provider{" + "name='" + this.name + ", parameters=" + this.parameters + + '}'; + } + } + } diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java index bb1692fa77c..666058ff4fd 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshaller.java @@ -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 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 providers = new ArrayList(); + 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 parameters = new HashMap(); + 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; diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java index ac85dfb755d..d174529385a 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataAnnotationProcessorTests.java @@ -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.emptyList(), Arrays.asList( + new ItemHint.ProviderHint("first", Collections.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, diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java index 49f7a5f2d87..81a951c3bbf 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConfigurationMetadataMatchers.java @@ -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 values; + private final List providers; + public ContainsHintMatcher(String name) { - this(name, new ArrayList()); + this(name, new ArrayList(), new ArrayList()); } - public ContainsHintMatcher(String name, List values) { + public ContainsHintMatcher(String name, List values, List 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 values = new ArrayList(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 parameters) { + List providers = new ArrayList(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 { + private final int index; + private final String name; + private final Map parameters; + + public ProviderHintMatcher(int index, String name, Map 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 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); + } + } + + } + } diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java index 8a05d700279..bed6cc1e4ef 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/metadata/JsonMarshallerTests.java @@ -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.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")); } }