Auto-generate the "Common application properties"

Prior to this commit, the application properties listed in the reference
documentation would be manually managed and updated.

This commit adds a new `spring-boot-configuration-docs` project that
extracts that information from the available JSON metadata and writes
Asciidoctor tables ready for inclusion in the reference documentation.

The `generateConfigurationPropertyTables.groovy` is using this library
and configures the sections and how namespaces should be organized.

Fixes gh-8237
This commit is contained in:
Brian Clozel
2019-02-18 15:34:53 +01:00
parent add8c6f295
commit 2a2bfb9915
15 changed files with 892 additions and 1549 deletions

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2012-2019 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.configurationdocs;
import java.util.Objects;
/**
* Abstract class for entries in {@link ConfigurationTable}.
*
* @author Brian Clozel
*/
abstract class AbstractConfigurationEntry
implements Comparable<AbstractConfigurationEntry> {
protected static final String NEWLINE = System.lineSeparator();
protected String key;
public String getKey() {
return this.key;
}
public abstract void writeAsciidoc(StringBuilder builder);
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
AbstractConfigurationEntry that = (AbstractConfigurationEntry) o;
return this.key.equals(that.key);
}
@Override
public int hashCode() {
return Objects.hash(this.key);
}
@Override
public int compareTo(AbstractConfigurationEntry other) {
return this.key.compareTo(other.getKey());
}
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012-2019 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.configurationdocs;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Stream;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
/**
* Table entry regrouping a list of configuration properties sharing the same description.
*
* @author Brian Clozel
*/
class CompoundKeyEntry extends AbstractConfigurationEntry {
private Set<String> configurationKeys;
private String description;
CompoundKeyEntry(String key, String description) {
this.key = key;
this.description = description;
this.configurationKeys = new TreeSet<>();
}
void addConfigurationKeys(ConfigurationMetadataProperty... properties) {
Stream.of(properties)
.forEach((property) -> this.configurationKeys.add(property.getId()));
}
@Override
public void writeAsciidoc(StringBuilder builder) {
builder.append("|`+++");
this.configurationKeys.forEach((key) -> builder.append(key).append(NEWLINE));
builder.append("+++`").append(NEWLINE).append("|").append(NEWLINE).append("|+++")
.append(this.description).append("+++").append(NEWLINE);
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2012-2019 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.configurationdocs;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder;
/**
* Write Asciidoc documents with configuration properties listings.
*
* @author Brian Clozel
*/
public class ConfigurationMetadataDocumentWriter {
public void writeDocument(Path outputDirPath, DocumentOptions options,
InputStream... metadataInput) throws IOException {
if (outputDirPath == null) {
throw new IllegalArgumentException("output path should not be null");
}
if (Files.exists(outputDirPath) && !Files.isDirectory(outputDirPath)) {
throw new IllegalArgumentException(
"output path already exists and is not a directory");
}
else if (!Files.exists(outputDirPath)) {
Files.createDirectory(outputDirPath);
}
if (metadataInput == null || metadataInput.length < 1) {
throw new IllegalArgumentException("missing input metadata");
}
ConfigurationMetadataRepository configRepository = ConfigurationMetadataRepositoryJsonBuilder
.create(metadataInput).build();
Map<String, ConfigurationMetadataProperty> allProperties = configRepository
.getAllProperties();
List<ConfigurationTable> tables = createConfigTables(allProperties, options);
for (ConfigurationTable table : tables) {
Path outputFilePath = outputDirPath.resolve(table.getId() + ".adoc");
Files.deleteIfExists(outputFilePath);
Files.createFile(outputFilePath);
try (OutputStream outputStream = Files.newOutputStream(outputFilePath)) {
outputStream
.write(table.toAsciidocTable().getBytes(StandardCharsets.UTF_8));
}
}
}
private List<ConfigurationTable> createConfigTables(
Map<String, ConfigurationMetadataProperty> allProperties,
DocumentOptions options) {
final List<ConfigurationTable> tables = new ArrayList<>();
final List<String> unmappedKeys = allProperties.values().stream()
.filter((prop) -> !prop.isDeprecated()).map((prop) -> prop.getId())
.collect(Collectors.toList());
final Map<String, CompoundKeyEntry> overrides = getOverrides(allProperties,
unmappedKeys, options);
options.getMetadataSections().forEach((id, keyPrefixes) -> {
ConfigurationTable table = new ConfigurationTable(id);
tables.add(table);
for (String keyPrefix : keyPrefixes) {
List<String> matchingOverrides = overrides.keySet().stream()
.filter((overrideKey) -> overrideKey.startsWith(keyPrefix))
.collect(Collectors.toList());
matchingOverrides.forEach((match) -> {
table.addEntry(overrides.remove(match));
});
}
List<String> matchingKeys = unmappedKeys.stream()
.filter((key) -> keyPrefixes.stream().anyMatch(key::startsWith))
.collect(Collectors.toList());
for (String matchingKey : matchingKeys) {
ConfigurationMetadataProperty property = allProperties.get(matchingKey);
table.addEntry(new SingleKeyEntry(property));
}
unmappedKeys.removeAll(matchingKeys);
});
if (!unmappedKeys.isEmpty()) {
throw new IllegalStateException(
"The following keys were not written to the documentation: "
+ String.join(", ", unmappedKeys));
}
if (!overrides.isEmpty()) {
throw new IllegalStateException(
"The following keys were not written to the documentation: "
+ String.join(", ", overrides.keySet()));
}
return tables;
}
private Map<String, CompoundKeyEntry> getOverrides(
Map<String, ConfigurationMetadataProperty> allProperties,
List<String> unmappedKeys, DocumentOptions options) {
final Map<String, CompoundKeyEntry> overrides = new HashMap<>();
options.getOverrides().forEach((keyPrefix, description) -> {
final CompoundKeyEntry entry = new CompoundKeyEntry(keyPrefix, description);
List<String> matchingKeys = unmappedKeys.stream()
.filter((key) -> key.startsWith(keyPrefix))
.collect(Collectors.toList());
for (String matchingKey : matchingKeys) {
entry.addConfigurationKeys(allProperties.get(matchingKey));
}
overrides.put(keyPrefix, entry);
unmappedKeys.removeAll(matchingKeys);
});
return overrides;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2012-2019 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.configurationdocs;
import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
/**
* Asciidoctor table listing configuration properties sharing to a common theme.
*
* @author Brian Clozel
*/
class ConfigurationTable {
private static final String NEWLINE = System.lineSeparator();
private final String id;
private final Set<AbstractConfigurationEntry> entries;
ConfigurationTable(String id) {
this.id = id;
this.entries = new TreeSet<>();
}
public String getId() {
return this.id;
}
void addEntry(AbstractConfigurationEntry... entries) {
this.entries.addAll(Arrays.asList(entries));
}
String toAsciidocTable() {
final StringBuilder builder = new StringBuilder();
builder.append("[cols=\"1,1,2\", options=\"header\"]").append(NEWLINE);
builder.append("|===").append(NEWLINE).append("|Key|Default Value|Description")
.append(NEWLINE).append(NEWLINE);
this.entries.forEach((entry) -> {
entry.writeAsciidoc(builder);
builder.append(NEWLINE);
});
return builder.append("|===").append(NEWLINE).toString();
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2012-2019 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.configurationdocs;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Options for generating documentation for configuration properties.
*
* @author Brian Clozel
*/
public final class DocumentOptions {
private final Map<String, List<String>> metadataSections;
private final Map<String, String> overrides;
private DocumentOptions(Map<String, List<String>> metadataSections,
Map<String, String> overrides) {
this.metadataSections = metadataSections;
this.overrides = overrides;
}
Map<String, List<String>> getMetadataSections() {
return this.metadataSections;
}
Map<String, String> getOverrides() {
return this.overrides;
}
static Builder builder() {
return new Builder();
}
/**
* Builder for DocumentOptions.
*/
public static class Builder {
Map<String, List<String>> metadataSections = new HashMap<>();
Map<String, String> overrides = new HashMap<>();
SectionSpec addSection(String name) {
return new SectionSpec(this, name);
}
Builder addOverride(String keyPrefix, String description) {
this.overrides.put(keyPrefix, description);
return this;
}
DocumentOptions build() {
return new DocumentOptions(this.metadataSections, this.overrides);
}
}
/**
* Configuration for a documentation section listing properties for a specific theme.
*/
public static class SectionSpec {
private final String name;
private final Builder builder;
SectionSpec(Builder builder, String name) {
this.builder = builder;
this.name = name;
}
Builder withKeyPrefixes(String... keyPrefixes) {
this.builder.metadataSections.put(this.name, Arrays.asList(keyPrefixes));
return this.builder;
}
}
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright 2012-2019 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.configurationdocs;
import java.util.Arrays;
import java.util.stream.Collectors;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
/**
* Table entry containing a single configuration property.
*
* @author Brian Clozel
*/
class SingleKeyEntry extends AbstractConfigurationEntry {
private String defaultValue;
private String description;
SingleKeyEntry(ConfigurationMetadataProperty property) {
this.key = property.getId();
if (property.getType() != null
&& property.getType().startsWith("java.util.Map")) {
this.key += ".*";
}
this.description = property.getDescription();
if (property.getDefaultValue() != null) {
if (property.getDefaultValue().getClass().isArray()) {
this.defaultValue = Arrays.stream((Object[]) property.getDefaultValue())
.map(Object::toString).collect(Collectors.joining("," + NEWLINE));
}
else {
this.defaultValue = property.getDefaultValue().toString();
}
}
}
@Override
public void writeAsciidoc(StringBuilder builder) {
builder.append("|`+").append(this.key).append("+`").append(NEWLINE);
String defaultValue = processDefaultValue();
if (defaultValue.length() > 0) {
builder.append("|`+").append(defaultValue).append("+`").append(NEWLINE);
}
else {
builder.append("|").append(NEWLINE);
}
if (this.description != null) {
builder.append("|+++").append(this.description).append("+++");
}
else {
builder.append("|");
}
builder.append(NEWLINE);
}
private String processDefaultValue() {
if (this.defaultValue != null && this.defaultValue.length() > 0) {
return this.defaultValue.replace("\\", "\\\\").replace("|",
"{vbar}" + NEWLINE);
}
return "";
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2012-2019 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.configurationdocs;
import org.junit.Test;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Brian Clozel
*/
public class CompoundKeyEntryTests {
private static String NEWLINE = System.lineSeparator();
@Test
public void simpleProperty() {
ConfigurationMetadataProperty firstProp = new ConfigurationMetadataProperty();
firstProp.setId("spring.test.first");
firstProp.setType("java.lang.String");
ConfigurationMetadataProperty secondProp = new ConfigurationMetadataProperty();
secondProp.setId("spring.test.second");
secondProp.setType("java.lang.String");
ConfigurationMetadataProperty thirdProp = new ConfigurationMetadataProperty();
thirdProp.setId("spring.test.third");
thirdProp.setType("java.lang.String");
CompoundKeyEntry entry = new CompoundKeyEntry("spring.test",
"This is a description.");
entry.addConfigurationKeys(firstProp, secondProp, thirdProp);
StringBuilder builder = new StringBuilder();
entry.writeAsciidoc(builder);
assertThat(builder.toString()).isEqualTo("|`+++spring.test.first" + NEWLINE
+ "spring.test.second" + NEWLINE + "spring.test.third" + NEWLINE + "+++`"
+ NEWLINE + "|" + NEWLINE + "|+++This is a description.+++" + NEWLINE);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2012-2019 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.configurationdocs;
import org.junit.Test;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Brian Clozel
*/
public class ConfigurationTableTests {
private static String NEWLINE = System.lineSeparator();
@Test
public void simpleTable() {
ConfigurationTable table = new ConfigurationTable("test");
ConfigurationMetadataProperty first = new ConfigurationMetadataProperty();
first.setId("spring.test.prop");
first.setDefaultValue("something");
first.setDescription("This is a description.");
first.setType("java.lang.String");
ConfigurationMetadataProperty second = new ConfigurationMetadataProperty();
second.setId("spring.test.other");
second.setDefaultValue("other value");
second.setDescription("This is another description.");
second.setType("java.lang.String");
table.addEntry(new SingleKeyEntry(first));
table.addEntry(new SingleKeyEntry(second));
assertThat(table.toAsciidocTable())
.isEqualTo("[cols=\"1,1,2\", options=\"header\"]" + NEWLINE + "|==="
+ NEWLINE + "|Key|Default Value|Description" + NEWLINE + NEWLINE
+ "|`+spring.test.other+`" + NEWLINE + "|`+other value+`"
+ NEWLINE + "|+++This is another description.+++" + NEWLINE
+ NEWLINE + "|`+spring.test.prop+`" + NEWLINE + "|`+something+`"
+ NEWLINE + "|+++This is a description.+++" + NEWLINE + NEWLINE
+ "|===" + NEWLINE);
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright 2012-2019 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.configurationdocs;
import org.junit.Test;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Brian Clozel
*/
public class SingleKeyEntryTests {
private static String NEWLINE = System.lineSeparator();
@Test
public void simpleProperty() {
ConfigurationMetadataProperty property = new ConfigurationMetadataProperty();
property.setId("spring.test.prop");
property.setDefaultValue("something");
property.setDescription("This is a description.");
property.setType("java.lang.String");
SingleKeyEntry entry = new SingleKeyEntry(property);
StringBuilder builder = new StringBuilder();
entry.writeAsciidoc(builder);
assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE
+ "|`+something+`" + NEWLINE + "|+++This is a description.+++" + NEWLINE);
}
@Test
public void noDefaultValue() {
ConfigurationMetadataProperty property = new ConfigurationMetadataProperty();
property.setId("spring.test.prop");
property.setDescription("This is a description.");
property.setType("java.lang.String");
SingleKeyEntry entry = new SingleKeyEntry(property);
StringBuilder builder = new StringBuilder();
entry.writeAsciidoc(builder);
assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|"
+ NEWLINE + "|+++This is a description.+++" + NEWLINE);
}
@Test
public void defaultValueWithPipes() {
ConfigurationMetadataProperty property = new ConfigurationMetadataProperty();
property.setId("spring.test.prop");
property.setDefaultValue("first|second");
property.setDescription("This is a description.");
property.setType("java.lang.String");
SingleKeyEntry entry = new SingleKeyEntry(property);
StringBuilder builder = new StringBuilder();
entry.writeAsciidoc(builder);
assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE
+ "|`+first{vbar}" + NEWLINE + "second+`" + NEWLINE
+ "|+++This is a description.+++" + NEWLINE);
}
@Test
public void defaultValueWithBackslash() {
ConfigurationMetadataProperty property = new ConfigurationMetadataProperty();
property.setId("spring.test.prop");
property.setDefaultValue("first\\second");
property.setDescription("This is a description.");
property.setType("java.lang.String");
SingleKeyEntry entry = new SingleKeyEntry(property);
StringBuilder builder = new StringBuilder();
entry.writeAsciidoc(builder);
assertThat(builder.toString())
.isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|`+first\\\\second+`"
+ NEWLINE + "|+++This is a description.+++" + NEWLINE);
}
@Test
public void mapProperty() {
ConfigurationMetadataProperty property = new ConfigurationMetadataProperty();
property.setId("spring.test.prop");
property.setDescription("This is a description.");
property.setType("java.util.Map<java.lang.String,java.lang.String>");
SingleKeyEntry entry = new SingleKeyEntry(property);
StringBuilder builder = new StringBuilder();
entry.writeAsciidoc(builder);
assertThat(builder.toString()).isEqualTo("|`+spring.test.prop.*+`" + NEWLINE + "|"
+ NEWLINE + "|+++This is a description.+++" + NEWLINE);
}
@Test
public void listProperty() {
String[] defaultValue = new String[] { "first", "second", "third" };
ConfigurationMetadataProperty property = new ConfigurationMetadataProperty();
property.setId("spring.test.prop");
property.setDescription("This is a description.");
property.setType("java.util.List<java.lang.String>");
property.setDefaultValue(defaultValue);
SingleKeyEntry entry = new SingleKeyEntry(property);
StringBuilder builder = new StringBuilder();
entry.writeAsciidoc(builder);
assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE
+ "|`+first," + NEWLINE + "second," + NEWLINE + "third+`" + NEWLINE
+ "|+++This is a description.+++" + NEWLINE);
}
}