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 4b57037d98..805b33869c 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-configuration-metadata.adoc @@ -7,7 +7,9 @@ contextual help and "`code completion`" as users are working with `application.p or `application.yml` files. The majority of the meta-data file is generated automatically at compile time by -processing all items annotated with `@ConfigurationProperties`. +processing all items annotated with `@ConfigurationProperties`. However, it is possible +to <> +for corner cases or more advanced use cases. @@ -15,7 +17,8 @@ processing all items annotated with `@ConfigurationProperties`. === Meta-data format Configuration meta-data files are located inside jars under `META-INF/spring-configuration-metadata.json` They use a simple JSON format with items -categorized under either "`groups`" or "`properties`": +categorized under either "`groups`" or "`properties`" and additional values hint +categorized under "hints": [source,json,indent=0] ---- @@ -24,6 +27,12 @@ categorized under either "`groups`" or "`properties`": "name": "server", "type": "org.springframework.boot.autoconfigure.web.ServerProperties", "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties" + }, + { + "name": "server.tomcat", + "type": "org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat", + "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties", + "sourceMethod": "getTomcat()" } ... ],"properties": [ @@ -37,8 +46,33 @@ categorized under either "`groups`" or "`properties`": "type": "java.lang.String", "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties", "defaultValue": "/" - } + }, + { + "name": "server.tomcat.compression", + "type": "java.lang.String", + "description": "Controls response compression.", + "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat", + "defaultValue": "off" + } ... + ],"hints": [ + { + "name": "server.tomcat.compression", + "values": [ + { + "value": "off", + "description": "Disable compression." + }, + { + "value": "on", + "description": "Enable compression of responses over 2048 byte." + }, + { + "value": "force", + "description": "Enable compression of all responses." + }, + ] + } ]} ---- @@ -59,6 +93,9 @@ provide a contextual grouping for properties. For example the `server.port` and NOTE: It is not required that every "`property`" has a "`group`", some properties might just exist in their own right. +Finally, "`hints`" are additional information used to assist the user in configuring a +given property. When configuring the `server.tomcat.compression` property, a tool can +use it to offer some auto-completion help for the `off`, `on` and `force` values. [[configuration-metadata-group-attributes]] @@ -152,6 +189,46 @@ The JSON object contained in the `properties` array can contain the following at |=== +[[configuration-metadata-hints-attributes]] +==== Hint Attributes +The JSON object contained in the `hints` array can contain the following attributes: + +[cols="1,1,4"] +|=== +|Name | Type |Purpose + +|`name` +| String +| The full name of the property that this hint refers to. Names are in lowercase dashed + form (e.g. `server.servlet-path`). If the property refers to a map (e.g. + `system.contexts`) the hint either applies to the _keys_ of the map (`system.context.keys`) + or the values (`system.context.values`). This attribute is mandatory. + +|`values` +| ValueHint[] +| A list of valid values as defined by the `ValueHint` object (see below). Each entry defines + the value and may have a description +|=== + +The JSON object contained in the `values` array of each `hint` element can contain the +following attributes: + +[cols="1,1,4"] +|=== +|Name | Type |Purpose + +|`value` +| Object +| A valid value for the element to which the hint refers to. Can also be an array of value(s) + if the type of the property is an array. This attribute is mandatory. + +|`description` +| String +| A short description of the value that can be displayed to users. May be omitted if no + description is available. It is recommended that descriptions are a short paragraphs, + with the first line providing a concise summary. The last line in the description should + end with a period (`.`). +|=== [[configuration-metadata-repeated-items]] ==== Repeated meta-data items @@ -161,7 +238,19 @@ appear multiple times within a meta-data file. For example, Spring Boot binds offering overlap of property names. Consumers of meta-data should take care to ensure that they support such scenarios. +=== 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. + +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`, +`off` and `force`. + +If your property is of type `Map`, you can provide hints for both the keys and the +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. [[configuration-metadata-annotation-processor]] === Generating your own meta-data using the annotation processor @@ -250,8 +339,9 @@ if it were nested. ==== Adding additional meta-data Spring Boot's configuration file handling is quite flexible; and it often the case that properties may exist that are not bound to a `@ConfigurationProperties` bean. To support -such cases, the annotation processor will automatically merge items from -`META-INF/additional-spring-configuration-metadata.json` into the main meta-data file. +such cases and allow you to provide custom "hints", the annotation processor will +automatically merge items from `META-INF/additional-spring-configuration-metadata.json` +into the main meta-data file. The format of the `additional-spring-configuration-metadata.json` file is exactly the same as the regular `spring-configuration-metadata.json`. The additional properties file is diff --git a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java index 878b40ce8f..ac87835994 100644 --- a/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ConfigurationMetadata.java @@ -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 items; + private final List hints; + public ConfigurationMetadata() { this.items = new ArrayList(); + this.hints = new ArrayList(); } public ConfigurationMetadata(ConfigurationMetadata metadata) { this.items = new ArrayList(metadata.getItems()); + this.hints = new ArrayList(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 getHints() { + return Collections.unmodifiableList(this.hints); + } + public static String nestedPrefix(String prefix, String name) { String nestedPrefix = (prefix == null ? "" : prefix); String dashedName = toDashedCase(name); 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 new file mode 100644 index 0000000000..cee2ddb598 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/metadata/ItemHint.java @@ -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. + *

+ * 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 { + + private final String name; + + private final List values; + + public ItemHint(String name, List values) { + this.name = toCanonicalName(name); + this.values = new ArrayList(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 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 + '\'' + + '}'; + } + } +} 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 843f0a7613..4483274ea9 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 @@ -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 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 values = new ArrayList(); + 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 { 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 93aa9f6490..c044262103 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 @@ -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 { } 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 adfd99d52c..680ddc7407 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 @@ -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 { 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 { + + private final String name; + + private final List values; + + public ContainsHintMatcher(String name) { + this(name, new ArrayList()); + } + + public ContainsHintMatcher(String name, List 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 values = new ArrayList(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 { + 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; + } + + } + } 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 298f8d461a..6ab1f7e041 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 @@ -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)); } }