diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index 68a9a5182e..107b4c5ea2 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -189,6 +189,11 @@ spring-boot-autoconfigure 1.3.0.BUILD-SNAPSHOT + + org.springframework.boot + spring-boot-configuration-metadata + 1.3.0.BUILD-SNAPSHOT + org.springframework.boot spring-boot-configuration-processor diff --git a/spring-boot-tools/pom.xml b/spring-boot-tools/pom.xml index 370869d27d..8adb891658 100644 --- a/spring-boot-tools/pom.xml +++ b/spring-boot-tools/pom.xml @@ -20,6 +20,7 @@ ${basedir}/.. + spring-boot-configuration-metadata spring-boot-configuration-processor spring-boot-loader spring-boot-loader-tools diff --git a/spring-boot-tools/spring-boot-configuration-metadata/pom.xml b/spring-boot-tools/spring-boot-configuration-metadata/pom.xml new file mode 100644 index 0000000000..0449ea90ab --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/pom.xml @@ -0,0 +1,29 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-tools + 1.3.0.BUILD-SNAPSHOT + + spring-boot-configuration-metadata + Spring Boot Configuration Metadata + Spring Boot Configuration Metadata + http://projects.spring.io/spring-boot/ + + Pivotal Software, Inc. + http://www.spring.io + + + ${basedir}/../.. + + + + + org.json + json + + + + diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataGroup.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataGroup.java new file mode 100644 index 0000000000..bc6009d9fa --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataGroup.java @@ -0,0 +1,71 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.util.HashMap; +import java.util.Map; + +/** + * Gather a collection of {@link ConfigurationMetadataProperty properties} that + * are sharing a {@link #getId() common prefix}. Provide access to all the + * {@link ConfigurationMetadataSource sources} that have contributed properties + * to the group. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class ConfigurationMetadataGroup { + + private final String id; + + private final Map sources = + new HashMap(); + + private final Map properties = + new HashMap(); + + public ConfigurationMetadataGroup(String id) { + this.id = id; + } + + /** + * Return the id of the group, used as a common prefix for all properties + * associated to it. + */ + public String getId() { + return this.id; + } + + /** + * Return the {@link ConfigurationMetadataSource sources} defining + * the properties of this group. + */ + public Map getSources() { + return this.sources; + } + + /** + * Return the {@link ConfigurationMetadataProperty properties} defined in this group. + *

A property may appear more than once for a given source, potentially with conflicting + * type or documentation. This is a "merged" view of the properties of this group. + * @see ConfigurationMetadataSource#getProperties() + */ + public Map getProperties() { + return this.properties; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataHint.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataHint.java new file mode 100644 index 0000000000..ac0475fbaf --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataHint.java @@ -0,0 +1,36 @@ +package org.springframework.boot.configurationmetadata; + +import java.util.ArrayList; +import java.util.List; + +/** + * A raw view of a hint used for parsing only. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +class ConfigurationMetadataHint { + + private String id; + + private final List valueHints = new ArrayList(); + + private final List valueProviders = new ArrayList(); + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public List getValueHints() { + return valueHints; + } + + public List getValueProviders() { + return valueProviders; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataItem.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataItem.java new file mode 100644 index 0000000000..cdc21a178b --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataItem.java @@ -0,0 +1,58 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +/** + * An extension of {@link ConfigurationMetadataProperty} that provides the + * a reference to its source. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +class ConfigurationMetadataItem extends ConfigurationMetadataProperty { + + private String sourceType; + + private String sourceMethod; + + /** + * The class name of the source that contributed this property. For example, if the property + * was from a class annotated with {@code @ConfigurationProperties} this attribute would + * contain the fully qualified name of that class. + */ + public String getSourceType() { + return this.sourceType; + } + + public void setSourceType(String sourceType) { + this.sourceType = sourceType; + } + + /** + * The full name of the method (including parenthesis and argument types) that contributed this + * property. For example, the name of a getter in a {@code @ConfigurationProperties} annotated + * class. + */ + public String getSourceMethod() { + return this.sourceMethod; + } + + public void setSourceMethod(String sourceMethod) { + this.sourceMethod = sourceMethod; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataProperty.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataProperty.java new file mode 100644 index 0000000000..45c134fb6b --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataProperty.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.util.ArrayList; +import java.util.List; + +/** + * Define a configuration property. Each property is fully identified by + * its {@link #getId() id} who is composed of a namespace prefix (the + * {@link ConfigurationMetadataGroup#getId() group id}), if any and the + * {@link #getName() name} of the property. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class ConfigurationMetadataProperty { + + private String id; + + private String name; + + private String type; + + private String description; + + private String shortDescription; + + private Object defaultValue; + + private final List valueHints = new ArrayList(); + + private final List valueProviders = new ArrayList(); + + private boolean deprecated; + + /** + * The full identifier of the property, in lowercase dashed form (e.g. my.group.simple-property) + */ + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + /** + * The name of the property, in lowercase dashed form (e.g. simple-property). If this item + * does not belong to any group, the id is returned. + */ + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * The class name of the data type of the property. For example, {@code java.lang.String}. + *

For consistency, the type of a primitive is specified using its wrapper counterpart, + * i.e. {@code boolean} becomes {@code java.lang.Boolean}. If the type holds generic + * information, these are provided as well, i.e. a {@code HashMap} of String to Integer + * would be defined as {@code java.util.HashMap}. + *

Note that this class may be a complex type that gets converted from a String as values + * are bound. + */ + public String getType() { + return this.type; + } + + public void setType(String type) { + this.type = type; + } + + /** + * A description of the property, if any. Can be multi-lines. + * @see #getShortDescription() + */ + public String getDescription() { + return this.description; + } + + public void setDescription(String description) { + this.description = description; + } + + /** + * A single-line, single-sentence description of this property, if any. + * @see #getDescription() + */ + public String getShortDescription() { + return shortDescription; + } + + public void setShortDescription(String shortDescription) { + this.shortDescription = shortDescription; + } + + /** + * The default value, if any. + */ + public Object getDefaultValue() { + return this.defaultValue; + } + + public void setDefaultValue(Object defaultValue) { + this.defaultValue = defaultValue; + } + + /** + * The list of well-defined values, if any. If no extra {@link ValueProvider provider} is + * specified, these values are to be considered a closed-set of the available values + * for this item. + */ + public List getValueHints() { + return valueHints; + } + + /** + * The value providers that are applicable to this item. Only one {@link ValueProvider} is + * enabled for an item: the first in the list that is supported should be used. + */ + public List getValueProviders() { + return valueProviders; + } + + /** + * Specify if the property is deprecated. + */ + public boolean isDeprecated() { + return this.deprecated; + } + + public void setDeprecated(boolean deprecated) { + this.deprecated = deprecated; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepository.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepository.java new file mode 100644 index 0000000000..42db7f099c --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepository.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.util.Map; + +/** + * A repository of configuration metadata. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public interface ConfigurationMetadataRepository { + + /** + * Defines the name of the "root" group, that is the group that + * gathers all the properties that aren't attached to a specific + * group. + */ + String ROOT_GROUP = "_ROOT_GROUP_"; + + /** + * Return the groups, indexed by id. + */ + Map getAllGroups(); + + /** + * Return the properties, indexed by id. + */ + Map getAllProperties(); + +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilder.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilder.java new file mode 100644 index 0000000000..5d13fc066b --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilder.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.json.JSONException; + +/** + * Load a {@link ConfigurationMetadataRepository} from the content of arbitrary resource(s). + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class ConfigurationMetadataRepositoryJsonBuilder { + + public static final Charset UTF_8 = Charset.forName("UTF-8"); + + private Charset defaultCharset = UTF_8; + + private final JsonReader reader = new JsonReader(); + + private final List repositories + = new ArrayList(); + + + /** + * Create a new builder instance using {@link #UTF_8} as the default charset. + */ + public static ConfigurationMetadataRepositoryJsonBuilder create() { + return create(UTF_8); + } + + /** + * Create a new builder instance using the specified default {@link Charset}. + */ + public static ConfigurationMetadataRepositoryJsonBuilder create(Charset defaultCharset) { + return new ConfigurationMetadataRepositoryJsonBuilder(defaultCharset); + } + + private ConfigurationMetadataRepositoryJsonBuilder(Charset defaultCharset) { + this.defaultCharset = defaultCharset; + } + + /** + * Add the content of a {@link ConfigurationMetadataRepository} defined by the specified + * {@link InputStream} json document using the default charset. If this metadata + * repository holds items that were loaded previously, these are ignored. + *

Leave the stream open when done. + */ + public ConfigurationMetadataRepositoryJsonBuilder withJsonResource(InputStream in) + throws IOException { + return withJsonResource(in, defaultCharset); + } + + /** + * Add the content of a {@link ConfigurationMetadataRepository} defined by the specified + * {@link InputStream} json document using the specified {@link Charset}. If this metadata + * repository holds items that were loaded previously, these are ignored. + *

Leave the stream open when done. + */ + public ConfigurationMetadataRepositoryJsonBuilder withJsonResource(InputStream inputstream, Charset charset) + throws IOException { + if (inputstream == null) { + throw new IllegalArgumentException("InputStream must not be null."); + } + repositories.add(add(inputstream, charset)); + return this; + } + + /** + * Build a {@link ConfigurationMetadataRepository} with the current state of this builder. + */ + public ConfigurationMetadataRepository build() { + SimpleConfigurationMetadataRepository result = new SimpleConfigurationMetadataRepository(); + for (SimpleConfigurationMetadataRepository repository : repositories) { + result.include(repository); + } + return result; + } + + private SimpleConfigurationMetadataRepository add(InputStream in, Charset charset) throws IOException { + try { + RawConfigurationMetadata metadata = this.reader.read(in, charset); + return create(metadata); + } + catch (IOException e) { + throw new IllegalArgumentException("Failed to read configuration metadata", e); + } + catch (JSONException e) { + throw new IllegalArgumentException("Invalid configuration metadata document", e); + } + } + + private SimpleConfigurationMetadataRepository create(RawConfigurationMetadata metadata) { + SimpleConfigurationMetadataRepository repository = new SimpleConfigurationMetadataRepository(); + repository.add(metadata.getSources()); + for (ConfigurationMetadataItem item : metadata.getItems()) { + ConfigurationMetadataSource source = null; + String sourceType = item.getSourceType(); + if (sourceType != null) { + source = metadata.getSource(sourceType); + } + repository.add(item, source); + } + Map allProperties = repository.getAllProperties(); + for (ConfigurationMetadataHint hint : metadata.getHints()) { + ConfigurationMetadataProperty property = allProperties.get(hint.getId()); + if (property != null) { + property.getValueHints().addAll(hint.getValueHints()); + property.getValueProviders().addAll(hint.getValueProviders()); + } + } + return repository; + } + +} + diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataSource.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataSource.java new file mode 100644 index 0000000000..2fa3fc4593 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataSource.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.util.HashMap; +import java.util.Map; + +/** + * A source of configuration metadata. Also defines where the source is declared, + * for instance if it is defined as a {@code @Bean}. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class ConfigurationMetadataSource { + + private String groupId; + + private String type; + + private String description; + + private String shortDescription; + + private String sourceType; + + private String sourceMethod; + + private final Map properties + = new HashMap(); + + /** + * The identifier of the group to which this source is associated + */ + public String getGroupId() { + return this.groupId; + } + + void setGroupId(String groupId) { + this.groupId = groupId; + } + + /** + * The type of the source. Usually this is the fully qualified name of a + * class that defines configuration items. This class may or may not be + * available at runtime. + */ + public String getType() { + return this.type; + } + + void setType(String type) { + this.type = type; + } + + /** + * A description of this source, if any. Can be multi-lines. + * @see #getShortDescription() + */ + public String getDescription() { + return this.description; + } + + void setDescription(String description) { + this.description = description; + } + + /** + * A single-line, single-sentence description of this source, if any. + * @see #getDescription() + */ + public String getShortDescription() { + return shortDescription; + } + + public void setShortDescription(String shortDescription) { + this.shortDescription = shortDescription; + } + + /** + * The type where this source is defined. This can be identical + * to the {@link #getType() type} if the source is self-defined. + */ + public String getSourceType() { + return this.sourceType; + } + + void setSourceType(String sourceType) { + this.sourceType = sourceType; + } + + /** + * The method name that defines this source, if any. + */ + public String getSourceMethod() { + return this.sourceMethod; + } + + void setSourceMethod(String sourceMethod) { + this.sourceMethod = sourceMethod; + } + + /** + * Return the properties defined by this source. + */ + public Map getProperties() { + return this.properties; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java new file mode 100644 index 0000000000..3921b4201c --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/JsonReader.java @@ -0,0 +1,206 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.text.BreakIterator; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +import org.json.JSONArray; +import org.json.JSONObject; + +/** + * Read standard json metadata format as {@link ConfigurationMetadataRepository} + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +class JsonReader { + + private static final int BUFFER_SIZE = 4096; + + private static final String NEW_LINE = System.getProperty("line.separator"); + + public RawConfigurationMetadata read(InputStream in, Charset charset) throws IOException { + JSONObject json = readJson(in, charset); + List groups = parseAllSources(json); + List items = parseAllItems(json); + List hints = parseAllHints(json); + return new RawConfigurationMetadata(groups, items, hints); + } + + private List parseAllSources(JSONObject root) { + List result = new ArrayList(); + if (!root.has("groups")) { + return result; + } + JSONArray sources = root.getJSONArray("groups"); + for (int i = 0; i < sources.length(); i++) { + JSONObject source = sources.getJSONObject(i); + result.add(parseSource(source)); + } + return result; + } + + private List parseAllItems(JSONObject root) { + List result = new ArrayList(); + if (!root.has("properties")) { + return result; + } + JSONArray items = root.getJSONArray("properties"); + for (int i = 0; i < items.length(); i++) { + JSONObject item = items.getJSONObject(i); + result.add(parseItem(item)); + } + return result; + } + + private List parseAllHints(JSONObject root) { + List result = new ArrayList(); + if (!root.has("hints")) { + return result; + } + JSONArray items = root.getJSONArray("hints"); + for (int i = 0; i < items.length(); i++) { + JSONObject item = items.getJSONObject(i); + result.add(parseHint(item)); + } + return result; + } + + private ConfigurationMetadataSource parseSource(JSONObject json) { + ConfigurationMetadataSource source = new ConfigurationMetadataSource(); + source.setGroupId(json.getString("name")); + source.setType(json.optString("type", null)); + String description = json.optString("description", null); + source.setDescription(description); + source.setShortDescription(extractShortDescription(description)); + source.setSourceType(json.optString("sourceType", null)); + source.setSourceMethod(json.optString("sourceMethod", null)); + return source; + } + + private ConfigurationMetadataItem parseItem(JSONObject json) { + ConfigurationMetadataItem item = new ConfigurationMetadataItem(); + item.setId(json.getString("name")); + item.setType(json.optString("type", null)); + String description = json.optString("description", null); + item.setDescription(description); + item.setShortDescription(extractShortDescription(description)); + item.setDefaultValue(readItemValue(json.opt("defaultValue"))); + item.setDeprecated(json.optBoolean("deprecated", false)); + item.setSourceType(json.optString("sourceType", null)); + item.setSourceMethod(json.optString("sourceMethod", null)); + return item; + } + + private ConfigurationMetadataHint parseHint(JSONObject json) { + ConfigurationMetadataHint hint = new ConfigurationMetadataHint(); + hint.setId(json.getString("name")); + if (json.has("values")) { + JSONArray values = json.getJSONArray("values"); + for (int i = 0; i < values.length(); i++) { + JSONObject value = values.getJSONObject(i); + ValueHint valueHint = new ValueHint(); + valueHint.setValue(readItemValue(value.get("value"))); + String description = value.optString("description", null); + valueHint.setDescription(description); + valueHint.setShortDescription(extractShortDescription(description)); + hint.getValueHints().add(valueHint); + } + } + if (json.has("providers")) { + JSONArray providers = json.getJSONArray("providers"); + for (int i = 0; i < providers.length(); i++) { + JSONObject provider = providers.getJSONObject(i); + ValueProvider valueProvider = new ValueProvider(); + valueProvider.setName(provider.getString("name")); + if (provider.has("parameters")) { + JSONObject parameters = provider.getJSONObject("parameters"); + Iterator keys = parameters.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + valueProvider.getParameters().put(key, readItemValue(parameters.get(key))); + } + } + hint.getValueProviders().add(valueProvider); + } + } + return hint; + } + + 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 value; + } + + static String extractShortDescription(String description) { + if (description == null) { + return null; + } + int dot = description.indexOf("."); + if (dot != -1) { + BreakIterator breakIterator = BreakIterator.getSentenceInstance(Locale.US); + breakIterator.setText(description); + String text = description.substring(breakIterator.first(), breakIterator.next()).trim(); + return removeSpaceBetweenLine(text); + } + else { + String[] lines = description.split(NEW_LINE); + return lines[0].trim(); + } + } + + private static String removeSpaceBetweenLine(String text) { + String[] lines = text.split(NEW_LINE); + StringBuilder sb = new StringBuilder(); + for (String line : lines) { + sb.append(line.trim()).append(" "); + } + return sb.toString().trim(); + } + + private JSONObject readJson(InputStream in, Charset charset) throws IOException { + try { + StringBuilder out = new StringBuilder(); + InputStreamReader reader = new InputStreamReader(in, charset); + char[] buffer = new char[BUFFER_SIZE]; + int bytesRead = -1; + while ((bytesRead = reader.read(buffer)) != -1) { + out.append(buffer, 0, bytesRead); + } + return new JSONObject(out.toString()); + } + finally { + in.close(); + } + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/RawConfigurationMetadata.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/RawConfigurationMetadata.java new file mode 100644 index 0000000000..0f3aa6ba01 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/RawConfigurationMetadata.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.util.ArrayList; +import java.util.List; + +/** + * A raw metadata structure. Used to initialize a {@link ConfigurationMetadataRepository}. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +class RawConfigurationMetadata { + + private final List sources; + + private final List items; + + private final List hints; + + RawConfigurationMetadata(List sources, + List items, List hints) { + this.sources = new ArrayList(sources); + this.items = new ArrayList(items); + this.hints = new ArrayList(hints); + for (ConfigurationMetadataItem item : this.items) { + resolveName(item); + } + } + + public List getSources() { + return this.sources; + } + + public ConfigurationMetadataSource getSource(String type) { + for (ConfigurationMetadataSource source : this.sources) { + if (type.equals(source.getType())) { + return source; + } + } + return null; + } + + public List getItems() { + return this.items; + } + + public List getHints() { + return hints; + } + + /** + * Resolve the name of an item against this instance. + * @see ConfigurationMetadataProperty#setName(String) + */ + private void resolveName(ConfigurationMetadataItem item) { + item.setName(item.getId()); // fallback + if (item.getSourceType() == null) { + return; + } + ConfigurationMetadataSource source = getSource(item.getSourceType()); + if (source != null) { + String groupId = source.getGroupId(); + String id = item.getId(); + if (id.startsWith(groupId)) { // match + String name = id.substring(groupId.length() + 1, id.length()); // "." + item.setName(name); + } + } + } +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java new file mode 100644 index 0000000000..b8fbae69fb --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/SimpleConfigurationMetadataRepository.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * The default {@link ConfigurationMetadataRepository} implementation. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class SimpleConfigurationMetadataRepository implements ConfigurationMetadataRepository { + + private final Map allGroups = new HashMap(); + + @Override + public Map getAllGroups() { + return Collections.unmodifiableMap(this.allGroups); + } + + @Override + public Map getAllProperties() { + Map properties = new HashMap(); + for (ConfigurationMetadataGroup group : this.allGroups.values()) { + properties.putAll(group.getProperties()); + } + return properties; + } + + /** + * Register the specified {@link ConfigurationMetadataSource sources}. + */ + public void add(Collection sources) { + for (ConfigurationMetadataSource source : sources) { + String groupId = source.getGroupId(); + ConfigurationMetadataGroup group = this.allGroups.get(groupId); + if (group == null) { + group = new ConfigurationMetadataGroup(groupId); + this.allGroups.put(groupId, group); + } + String sourceType = source.getType(); + if (sourceType != null) { + putIfAbsent(group.getSources(), sourceType, source); + } + } + } + + /** + * Add a {@link ConfigurationMetadataProperty} with the {@link ConfigurationMetadataSource source} + * that defines it, if any. + */ + public void add(ConfigurationMetadataProperty property, ConfigurationMetadataSource source) { + if (source != null) { + putIfAbsent(source.getProperties(), property.getId(), property); + } + putIfAbsent(getGroup(source).getProperties(), property.getId(), property); + } + + + /** + * Merge the content of the specified repository to this repository. + */ + public void include(ConfigurationMetadataRepository repository) { + for (ConfigurationMetadataGroup group : repository.getAllGroups().values()) { + ConfigurationMetadataGroup existingGroup = this.allGroups.get(group.getId()); + if (existingGroup == null) { + this.allGroups.put(group.getId(), group); + } + else { + // Merge properties + for (Map.Entry entry : group.getProperties().entrySet()) { + putIfAbsent(existingGroup.getProperties(), entry.getKey(), entry.getValue()); + } + // Merge sources + for (Map.Entry entry : group.getSources().entrySet()) { + putIfAbsent(existingGroup.getSources(), entry.getKey(), entry.getValue()); + } + } + } + + } + + private ConfigurationMetadataGroup getGroup(ConfigurationMetadataSource source) { + if (source == null) { + ConfigurationMetadataGroup rootGroup = this.allGroups.get(ROOT_GROUP); + if (rootGroup == null) { + rootGroup = new ConfigurationMetadataGroup(ROOT_GROUP); + this.allGroups.put(ROOT_GROUP, rootGroup); + } + return rootGroup; + } + else { + return this.allGroups.get(source.getGroupId()); + } + } + + private void putIfAbsent(Map map, String key, V value) { + if (!map.containsKey(key)) { + map.put(key, value); + } + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueHint.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueHint.java new file mode 100644 index 0000000000..bb457a9375 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueHint.java @@ -0,0 +1,58 @@ +package org.springframework.boot.configurationmetadata; + +/** + * Hint for a value a given property may have. Provide the value and + * an optional description. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class ValueHint { + + private Object value; + + private String description; + + private String shortDescription; + + /** + * Return the hint value. + */ + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + /** + * A description of this value, if any. Can be multi-lines. + * @see #getShortDescription() + */ + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + /** + * A single-line, single-sentence description of this hint, if any. + * @see #getDescription() + */ + public String getShortDescription() { + return shortDescription; + } + + public void setShortDescription(String shortDescription) { + this.shortDescription = shortDescription; + } + + @Override + public String toString() { + return "ValueHint{" + "value=" + this.value + ", description='" + + this.description + '\'' + '}'; + } +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueProvider.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueProvider.java new file mode 100644 index 0000000000..2475931e2d --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/ValueProvider.java @@ -0,0 +1,46 @@ +package org.springframework.boot.configurationmetadata; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Define a component that is able to provide the values of a property. + *

+ * Each provider is defined by a {@code name} and can have an arbitrary + * number of {@code parameters}. The available providers are defined in + * the Spring Boot documentation. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +public class ValueProvider { + + private String name; + + private final Map parameters = new LinkedHashMap(); + + /** + * Return the name of the provider. + */ + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** + * Return the parameters. + */ + public Map getParameters() { + return parameters; + } + + @Override + public String toString() { + return "ValueProvider{" + "name='" + this.name + ", parameters=" + this.parameters + + '}'; + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/package-info.java b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/package-info.java new file mode 100644 index 0000000000..0004ccf1d4 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/main/java/org/springframework/boot/configurationmetadata/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +/** + * Spring Boot configuration meta-data parser. + */ +package org.springframework.boot.configurationmetadata; \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/AbstractConfigurationMetadataTests.java b/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/AbstractConfigurationMetadataTests.java new file mode 100644 index 0000000000..9ca2bcb1dc --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/AbstractConfigurationMetadataTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.io.IOException; +import java.io.InputStream; + +import org.junit.Rule; +import org.junit.rules.ExpectedException; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * + * @author Stephane Nicoll + */ +public abstract class AbstractConfigurationMetadataTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + protected void assertSource(ConfigurationMetadataSource actual, String groupId, String type, String sourceType) { + assertNotNull(actual); + assertEquals(groupId, actual.getGroupId()); + assertEquals(type, actual.getType()); + assertEquals(sourceType, actual.getSourceType()); + } + + protected void assertProperty(ConfigurationMetadataProperty actual, String id, String name, + Class type, Object defaultValue) { + assertNotNull(actual); + assertEquals(id, actual.getId()); + assertEquals(name, actual.getName()); + String typeName = type != null ? type.getName() : null; + assertEquals(typeName, actual.getType()); + assertEquals(defaultValue, actual.getDefaultValue()); + } + + protected void assertItem(ConfigurationMetadataItem actual, String sourceType) { + assertNotNull(actual); + assertEquals(sourceType, actual.getSourceType()); + } + + protected InputStream getInputStreamFor(String name) throws IOException { + Resource r = new ClassPathResource("metadata/configuration-metadata-" + name + ".json"); + return r.getInputStream(); + } +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilderTests.java b/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilderTests.java new file mode 100644 index 0000000000..b9fe5090a8 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/ConfigurationMetadataRepositoryJsonBuilderTests.java @@ -0,0 +1,200 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +/** + * Tests for {@link ConfigurationMetadataRepository}. + * + * @author Stephane Nicoll + */ +public class ConfigurationMetadataRepositoryJsonBuilderTests extends AbstractConfigurationMetadataTests { + + + @Test + public void nullResource() throws IOException { + thrown.expect(IllegalArgumentException.class); + ConfigurationMetadataRepositoryJsonBuilder.create().withJsonResource(null); + } + + @Test + public void simpleRepository() throws IOException { + InputStream foo = getInputStreamFor("foo"); + try { + ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create() + .withJsonResource(foo) + .build(); + validateFoo(repo); + assertEquals(1, repo.getAllGroups().size()); + + contains(repo.getAllProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter"); + assertEquals(3, repo.getAllProperties().size()); + } + finally { + foo.close(); + } + } + + @Test + public void severalRepositoriesNoConflict() throws IOException { + InputStream foo = getInputStreamFor("foo"); + InputStream bar = getInputStreamFor("bar"); + try { + ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create() + .withJsonResource(foo) + .withJsonResource(bar) + .build(); + validateFoo(repo); + validateBar(repo); + assertEquals(2, repo.getAllGroups().size()); + + contains(repo.getAllProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter", + "spring.bar.name", "spring.bar.description", "spring.bar.counter"); + assertEquals(6, repo.getAllProperties().size()); + } + finally { + foo.close(); + bar.close(); + } + } + + @Test + public void repositoryWithRoot() throws IOException { + InputStream foo = getInputStreamFor("foo"); + InputStream root = getInputStreamFor("root"); + try { + ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create() + .withJsonResource(foo) + .withJsonResource(root) + .build(); + validateFoo(repo); + assertEquals(2, repo.getAllGroups().size()); + + contains(repo.getAllProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter", + "spring.root.name", "spring.root2.name"); + assertEquals(5, repo.getAllProperties().size()); + } + finally { + foo.close(); + root.close(); + } + } + + @Test + public void severalRepositoriesIdenticalGroups() throws IOException { + InputStream foo = getInputStreamFor("foo"); + InputStream foo2 = getInputStreamFor("foo2"); + try { + ConfigurationMetadataRepository repo = ConfigurationMetadataRepositoryJsonBuilder.create() + .withJsonResource(foo) + .withJsonResource(foo2) + .build(); + assertEquals(1, repo.getAllGroups().size()); + ConfigurationMetadataGroup group = repo.getAllGroups().get("spring.foo"); + contains(group.getSources(), "org.acme.Foo", "org.acme.Foo2", "org.springframework.boot.FooProperties"); + assertEquals(3, group.getSources().size()); + contains(group.getProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter", + "spring.foo.enabled", "spring.foo.type"); + assertEquals(5, group.getProperties().size()); + contains(repo.getAllProperties(), "spring.foo.name", "spring.foo.description", "spring.foo.counter", + "spring.foo.enabled", "spring.foo.type"); + assertEquals(5, repo.getAllProperties().size()); + } + finally { + foo.close(); + foo2.close(); + } + } + + @Test + public void builderInstancesAreIsolated() throws IOException { + InputStream foo = getInputStreamFor("foo"); + InputStream bar = getInputStreamFor("bar"); + try { + ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create(); + ConfigurationMetadataRepository firstRepo = builder + .withJsonResource(foo) + .build(); + validateFoo(firstRepo); + + ConfigurationMetadataRepository secondRepo = builder + .withJsonResource(bar) + .build(); + validateFoo(secondRepo); + validateBar(secondRepo); + + // first repo not impacted by second build + assertNotEquals(firstRepo, secondRepo); + assertEquals(1, firstRepo.getAllGroups().size()); + assertEquals(3, firstRepo.getAllProperties().size()); + assertEquals(2, secondRepo.getAllGroups().size()); + assertEquals(6, secondRepo.getAllProperties().size()); + } + finally { + foo.close(); + bar.close(); + } + } + + private void validateFoo(ConfigurationMetadataRepository repo) { + ConfigurationMetadataGroup group = repo.getAllGroups().get("spring.foo"); + contains(group.getSources(), "org.acme.Foo", "org.springframework.boot.FooProperties"); + ConfigurationMetadataSource source = group.getSources().get("org.acme.Foo"); + contains(source.getProperties(), "spring.foo.name", "spring.foo.description"); + assertEquals(2, source.getProperties().size()); + ConfigurationMetadataSource source2 = group.getSources().get("org.springframework.boot.FooProperties"); + contains(source2.getProperties(), "spring.foo.name", "spring.foo.counter"); + assertEquals(2, source2.getProperties().size()); + validatePropertyHints(repo.getAllProperties().get("spring.foo.name"), 0, 0); + validatePropertyHints(repo.getAllProperties().get("spring.foo.description"), 0, 0); + validatePropertyHints(repo.getAllProperties().get("spring.foo.counter"), 1, 1); + } + + private void validateBar(ConfigurationMetadataRepository repo) { + ConfigurationMetadataGroup group = repo.getAllGroups().get("spring.bar"); + contains(group.getSources(), "org.acme.Bar", "org.springframework.boot.BarProperties"); + ConfigurationMetadataSource source = group.getSources().get("org.acme.Bar"); + contains(source.getProperties(), "spring.bar.name", "spring.bar.description"); + assertEquals(2, source.getProperties().size()); + ConfigurationMetadataSource source2 = group.getSources().get("org.springframework.boot.BarProperties"); + contains(source2.getProperties(), "spring.bar.name", "spring.bar.counter"); + assertEquals(2, source2.getProperties().size()); + validatePropertyHints(repo.getAllProperties().get("spring.bar.name"), 0, 0); + validatePropertyHints(repo.getAllProperties().get("spring.bar.description"), 2, 2); + validatePropertyHints(repo.getAllProperties().get("spring.bar.counter"), 0, 0); + } + + private void validatePropertyHints(ConfigurationMetadataProperty property, int valueHints, int valueProviders) { + assertEquals(valueHints, property.getValueHints().size()); + assertEquals(valueProviders, property.getValueHints().size()); + } + + private void contains(Map source, String... keys) { + for (String key : keys) { + assertTrue("Item '" + key + "' not found. Got " + source.keySet(), source.containsKey(key)); + } + } +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java b/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java new file mode 100644 index 0000000000..539b30e9b2 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/test/java/org/springframework/boot/configurationmetadata/JsonReaderTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2012-2014 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.configurationmetadata; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.List; + +import org.json.JSONException; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Tests for {@link JsonReader} + * + * @author Stephane Nicoll + */ +public class JsonReaderTests extends AbstractConfigurationMetadataTests { + + private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private final JsonReader reader = new JsonReader(); + + @Test + public void emptyMetadata() throws IOException { + RawConfigurationMetadata rawMetadata = readFor("empty"); + assertEquals(0, rawMetadata.getSources().size()); + assertEquals(0, rawMetadata.getItems().size()); + } + + @Test + public void invalidMetadata() throws IOException { + thrown.expect(JSONException.class); + readFor("invalid"); + } + + @Test + public void simpleMetadata() throws IOException { + RawConfigurationMetadata rawMetadata = readFor("foo"); + List sources = rawMetadata.getSources(); + assertEquals(2, sources.size()); + List items = rawMetadata.getItems(); + assertEquals(4, items.size()); + List hints = rawMetadata.getHints(); + assertEquals(1, hints.size()); + + ConfigurationMetadataSource source = sources.get(0); + assertSource(source, "spring.foo", "org.acme.Foo", "org.acme.config.FooApp"); + assertEquals("foo()", source.getSourceMethod()); + assertEquals("This is Foo.", source.getDescription()); + assertEquals("This is Foo.", source.getShortDescription()); + + ConfigurationMetadataItem item = items.get(0); + assertProperty(item, "spring.foo.name", "name", String.class, null); + assertItem(item, "org.acme.Foo"); + ConfigurationMetadataItem item2 = items.get(1); + assertProperty(item2, "spring.foo.description", "description", String.class, "FooBar"); + assertEquals("Foo description.", item2.getDescription()); + assertEquals("Foo description.", item2.getShortDescription()); + assertNull(item2.getSourceMethod()); + assertItem(item2, "org.acme.Foo"); + + ConfigurationMetadataHint hint = hints.get(0); + assertEquals("spring.foo.counter", hint.getId()); + assertEquals(1, hint.getValueHints().size()); + ValueHint valueHint = hint.getValueHints().get(0); + assertEquals(42, valueHint.getValue()); + assertEquals("Because that's the answer to any question, choose it. \nReally.", + valueHint.getDescription()); + assertEquals("Because that's the answer to any question, choose it.", + valueHint.getShortDescription()); + assertEquals(1, hint.getValueProviders().size()); + ValueProvider valueProvider = hint.getValueProviders().get(0); + assertEquals("handle-as", valueProvider.getName()); + assertEquals(1, valueProvider.getParameters().size()); + assertEquals(Integer.class.getName(), valueProvider.getParameters().get("target")); + } + + @Test + public void metadataHints() throws IOException { + RawConfigurationMetadata rawMetadata = readFor("bar"); + List hints = rawMetadata.getHints(); + assertEquals(1, hints.size()); + + ConfigurationMetadataHint hint = hints.get(0); + assertEquals("spring.bar.description", hint.getId()); + assertEquals(2, hint.getValueHints().size()); + ValueHint valueHint = hint.getValueHints().get(0); + assertEquals("one", valueHint.getValue()); + assertEquals("One.", valueHint.getDescription()); + ValueHint valueHint2 = hint.getValueHints().get(1); + assertEquals("two", valueHint2.getValue()); + assertEquals(null, valueHint2.getDescription()); + + assertEquals(2, hint.getValueProviders().size()); + ValueProvider valueProvider = hint.getValueProviders().get(0); + assertEquals("handle-as", valueProvider.getName()); + assertEquals(1, valueProvider.getParameters().size()); + assertEquals(String.class.getName(), valueProvider.getParameters().get("target")); + ValueProvider valueProvider2 = hint.getValueProviders().get(1); + assertEquals("any", valueProvider2.getName()); + assertEquals(0, valueProvider2.getParameters().size()); + } + + @Test + public void rootMetadata() throws IOException { + RawConfigurationMetadata rawMetadata = readFor("root"); + List sources = rawMetadata.getSources(); + assertEquals(0, sources.size()); + List items = rawMetadata.getItems(); + assertEquals(2, items.size()); + + ConfigurationMetadataItem item = items.get(0); + assertProperty(item, "spring.root.name", "spring.root.name", String.class, null); + } + + @Test + public void extractShortDescription() { + assertEquals("My short description.", JsonReader.extractShortDescription( + "My short description. More stuff.")); + } + + @Test + public void extractShortDescriptionNewLineBeforeDot() { + assertEquals("My short description.", JsonReader.extractShortDescription( + "My short\ndescription.\nMore stuff.")); + } + + @Test + public void extractShortDescriptionNewLineBeforeDotWithSpaces() { + assertEquals("My short description.", JsonReader.extractShortDescription( + "My short \n description. \nMore stuff.")); + } + + @Test + public void extractShortDescriptionNoDot() { + assertEquals("My short description", JsonReader.extractShortDescription( + "My short description")); + } + + @Test + public void extractShortDescriptionNoDotMultipleLines() { + assertEquals("My short description", JsonReader.extractShortDescription( + "My short description \n More stuff")); + } + + @Test + public void extractShortDescriptionNull() { + assertEquals(null, JsonReader.extractShortDescription(null)); + } + + RawConfigurationMetadata readFor(String path) throws IOException { + return this.reader.read(getInputStreamFor(path), DEFAULT_CHARSET); + } + +} diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-bar.json b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-bar.json new file mode 100644 index 0000000000..6f07ce19ba --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-bar.json @@ -0,0 +1,65 @@ +{ + "groups": [ + { + "name": "spring.bar", + "type": "org.acme.Bar", + "sourceType": "org.acme.config.BarApp", + "sourceMethod": "bar()", + "description": "This is Bar." + }, + { + "name": "spring.bar", + "type": "org.springframework.boot.BarProperties" + } + ], + "properties": [ + { + "name": "spring.bar.name", + "type": "java.lang.String", + "sourceType": "org.acme.Bar" + }, + { + "name": "spring.bar.description", + "type": "java.lang.String", + "sourceType": "org.acme.Bar", + "description": "Bar description.", + "defaultValue": "BarFoo" + }, + { + "name": "spring.bar.name", + "type": "java.lang.String", + "sourceType": "org.springframework.boot.BarProperties" + }, + { + "name": "spring.bar.counter", + "type": "java.lang.Integer", + "sourceType": "org.springframework.boot.BarProperties", + "defaultValue": 0 + } + ], + "hints": [ + { + "name": "spring.bar.description", + "values": [ + { + "value": "one", + "description": "One." + }, + { + "value": "two" + } + ], + "providers": [ + { + "name": "handle-as", + "parameters": { + "target": "java.lang.String" + } + }, + { + "name": "any" + } + ] + } + ] +} \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-empty.json b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-empty.json new file mode 100644 index 0000000000..b42f309e7a --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-empty.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-foo.json b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-foo.json new file mode 100644 index 0000000000..74b1c57bfb --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-foo.json @@ -0,0 +1,59 @@ +{ + "groups": [ + { + "name": "spring.foo", + "type": "org.acme.Foo", + "sourceType": "org.acme.config.FooApp", + "sourceMethod": "foo()", + "description": "This is Foo." + }, + { + "name": "spring.foo", + "type": "org.springframework.boot.FooProperties" + } + ], + "properties": [ + { + "name": "spring.foo.name", + "type": "java.lang.String", + "sourceType": "org.acme.Foo" + }, + { + "name": "spring.foo.description", + "type": "java.lang.String", + "sourceType": "org.acme.Foo", + "description": "Foo description.", + "defaultValue": "FooBar" + }, + { + "name": "spring.foo.name", + "type": "java.lang.String", + "sourceType": "org.springframework.boot.FooProperties" + }, + { + "name": "spring.foo.counter", + "type": "java.lang.Integer", + "sourceType": "org.springframework.boot.FooProperties", + "defaultValue": 0 + } + ], + "hints": [ + { + "name": "spring.foo.counter", + "values": [ + { + "value": 42, + "description": "Because that's the answer to any question, choose it. \nReally." + } + ], + "providers": [ + { + "name": "handle-as", + "parameters": { + "target": "java.lang.Integer" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-foo2.json b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-foo2.json new file mode 100644 index 0000000000..a57f4992cf --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-foo2.json @@ -0,0 +1,23 @@ +{ + "groups": [ + { + "name": "spring.foo", + "type": "org.acme.Foo2", + "sourceType": "org.acme.config.FooApp", + "sourceMethod": "foo2()", + "description": "This is Foo2." + } + ], + "properties": [ + { + "name": "spring.foo.enabled", + "type": "java.lang.Boolean", + "sourceType": "org.acme.Foo2" + }, + { + "name": "spring.foo.type", + "type": "java.lang.String", + "sourceType": "org.acme.Foo2" + } + ] +} \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-invalid.json b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-invalid.json new file mode 100644 index 0000000000..ca2f071115 --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-invalid.json @@ -0,0 +1,8 @@ +{ + "properties": [ + { + "type": "java.lang.String", + "sourceType": "org.acme.Invalid" + } + ] +} \ No newline at end of file diff --git a/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-root.json b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-root.json new file mode 100644 index 0000000000..8fced8db7c --- /dev/null +++ b/spring-boot-tools/spring-boot-configuration-metadata/src/test/resources/metadata/configuration-metadata-root.json @@ -0,0 +1,11 @@ +{ + "properties": [ + { + "name": "spring.root.name", + "type": "java.lang.String" + }, + { + "name": "spring.root2.name" + } + ] +} \ No newline at end of file