Skip to content

Commit

Permalink
Add config metadata changelog generator to main build
Browse files Browse the repository at this point in the history
Closes gh-21486
  • Loading branch information
wilkinsona committed Jun 30, 2023
1 parent b655523 commit 32b7b31
Show file tree
Hide file tree
Showing 11 changed files with 686 additions and 0 deletions.
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ include "spring-boot-project:spring-boot-tools:spring-boot-autoconfigure-process
include "spring-boot-project:spring-boot-tools:spring-boot-buildpack-platform"
include "spring-boot-project:spring-boot-tools:spring-boot-cli"
include "spring-boot-project:spring-boot-tools:spring-boot-configuration-metadata"
include "spring-boot-project:spring-boot-tools:spring-boot-configuration-metadata-changelog-generator"
include "spring-boot-project:spring-boot-tools:spring-boot-configuration-processor"
include "spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin"
include "spring-boot-project:spring-boot-tools:spring-boot-gradle-test-support"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
plugins {
id "java"
id "org.springframework.boot.conventions"
}

description = "Spring Boot Configuration Metadata Changelog Generator"

configurations {
oldMetadata
newMetadata
}

dependencies {
implementation(enforcedPlatform(project(":spring-boot-project:spring-boot-dependencies")))
implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-configuration-metadata"))

testImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-dependencies")))
testImplementation("org.assertj:assertj-core")
testImplementation("org.junit.jupiter:junit-jupiter")
}

if (project.hasProperty("oldVersion") && project.hasProperty("newVersion")) {
dependencies {
["spring-boot",
"spring-boot-actuator",
"spring-boot-actuator-autoconfigure",
"spring-boot-autoconfigure",
"spring-boot-devtools",
"spring-boot-test-autoconfigure"].each {
oldMetadata("org.springframework.boot:$it:$oldVersion")
newMetadata("org.springframework.boot:$it:$newVersion")
}
}

def prepareOldMetadata = tasks.register("prepareOldMetadata", Sync) {
from(configurations.oldMetadata)
if (project.hasProperty("oldVersion")) {
destinationDir = project.file("build/configuration-metadata-diff/$oldVersion")
}
}

def prepareNewMetadata = tasks.register("prepareNewMetadata", Sync) {
from(configurations.newMetadata)
if (project.hasProperty("newVersion")) {
destinationDir = project.file("build/configuration-metadata-diff/$newVersion")
}
}

tasks.register("generate", JavaExec) {
inputs.files(prepareOldMetadata, prepareNewMetadata)
outputs.file(project.file("build/configuration-metadata-changelog.adoc"))
classpath = sourceSets.main.runtimeClasspath
mainClass = 'org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataChangelogGenerator'
if (project.hasProperty("oldVersion") && project.hasProperty("newVersion")) {
args = [project.file("build/configuration-metadata-diff/$oldVersion"), project.file("build/configuration-metadata-diff/$newVersion"), project.file("build/configuration-metadata-changelog.adoc")]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Copyright 2012-2023 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
*
* https://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.changelog;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

/**
* Generates a configuration metadata changelog. Requires three arguments:
*
* <ol>
* <li>The path of a directory containing jar files from which the old metadata will be
* extracted
* <li>The path of a directory containing jar files from which the new metadata will be
* extracted
* <li>The path of a file to which the changelog will be written
* </ol>
*
* The name of each directory will be used to name the old and new metadata in the
* generated changelog
*
* @author Andy Wilkinson
*/
final class ConfigurationMetadataChangelogGenerator {

private ConfigurationMetadataChangelogGenerator() {

}

public static void main(String[] args) throws IOException {
ConfigurationMetadataDiff diff = ConfigurationMetadataDiff.of(
NamedConfigurationMetadataRepository.from(new File(args[0])),
NamedConfigurationMetadataRepository.from(new File(args[1])));
try (ConfigurationMetadataChangelogWriter writer = new ConfigurationMetadataChangelogWriter(
new FileWriter(new File(args[2])))) {
writer.write(diff);
}
System.out.println("\nConfiguration metadata changelog written to '" + args[2] + "'");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* Copyright 2012-2023 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
*
* https://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.changelog;

import java.io.PrintWriter;
import java.io.Writer;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import org.springframework.boot.configurationmetadata.Deprecation;
import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference;
import org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataDiff.Difference.Type;

/**
* Writes a configuration metadata changelog from a {@link ConfigurationMetadataDiff}.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
*/
class ConfigurationMetadataChangelogWriter implements AutoCloseable {

private final PrintWriter out;

ConfigurationMetadataChangelogWriter(Writer out) {
this.out = new PrintWriter(out);
}

void write(ConfigurationMetadataDiff diff) {
this.out.append(String.format("Configuration property changes between `%s` and " + "`%s`%n", diff.leftName(),
diff.rightName()));
this.out.append(System.lineSeparator());
this.out.append(String.format("== Deprecated in `%s`%n", diff.rightName()));
Map<Type, List<Difference>> differencesByType = differencesByType(diff);
writeDeprecatedProperties(differencesByType.get(Type.DEPRECATED));
this.out.append(System.lineSeparator());
this.out.append(String.format("== New in `%s`%n", diff.rightName()));
writeAddedProperties(differencesByType.get(Type.ADDED));
this.out.append(System.lineSeparator());
this.out.append(String.format("== Removed in `%s`%n", diff.rightName()));
writeRemovedProperties(differencesByType.get(Type.DELETED), differencesByType.get(Type.DEPRECATED));
}

private Map<Type, List<Difference>> differencesByType(ConfigurationMetadataDiff diff) {
Map<Type, List<Difference>> differencesByType = new HashMap<>();
for (Type type : Type.values()) {
differencesByType.put(type, new ArrayList<>());
}
for (Difference difference : diff.differences()) {
differencesByType.get(difference.type()).add(difference);
}
return differencesByType;
}

private void writeDeprecatedProperties(List<Difference> differences) {
if (differences.isEmpty()) {
this.out.append(String.format("None.%n"));
}
else {
List<Difference> properties = sortProperties(differences, Difference::right).stream()
.filter(this::isDeprecatedInRelease)
.collect(Collectors.toList());
this.out.append(String.format("|======================%n"));
this.out.append(String.format("|Key |Replacement |Reason%n"));
properties.forEach((diff) -> {
ConfigurationMetadataProperty property = diff.right();
writeDeprecatedProperty(property);
});
this.out.append(String.format("|======================%n"));
}
this.out.append(String.format("%n%n"));
}

private boolean isDeprecatedInRelease(Difference difference) {
return difference.right().getDeprecation() != null
&& Deprecation.Level.ERROR != difference.right().getDeprecation().getLevel();
}

private void writeAddedProperties(List<Difference> differences) {
if (differences.isEmpty()) {
this.out.append(String.format("None.%n"));
}
else {
List<Difference> properties = sortProperties(differences, Difference::right);
this.out.append(String.format("|======================%n"));
this.out.append(String.format("|Key |Default value |Description%n"));
properties.forEach((diff) -> writeRegularProperty(diff.right()));
this.out.append(String.format("|======================%n"));
}
this.out.append(String.format("%n%n"));
}

private void writeRemovedProperties(List<Difference> deleted, List<Difference> deprecated) {
List<Difference> removed = getRemovedProperties(deleted, deprecated);
if (removed.isEmpty()) {
this.out.append(String.format("None.%n"));
}
else {
this.out.append(String.format("|======================%n"));
this.out.append(String.format("|Key |Replacement |Reason%n"));
removed.forEach((property) -> writeDeprecatedProperty(
(property.right() != null) ? property.right() : property.left()));
this.out.append(String.format("|======================%n"));
}
}

private List<Difference> getRemovedProperties(List<Difference> deleted, List<Difference> deprecated) {
List<Difference> properties = new ArrayList<>(deleted);
properties.addAll(deprecated.stream().filter((p) -> !isDeprecatedInRelease(p)).collect(Collectors.toList()));
return sortProperties(properties,
(difference) -> (difference.left() != null) ? difference.left() : difference.right());
}

private void writeRegularProperty(ConfigurationMetadataProperty property) {
this.out.append("|`").append(property.getId()).append("` |");
if (property.getDefaultValue() != null) {
this.out.append("`").append(defaultValueToString(property.getDefaultValue())).append("`");
}
this.out.append(" |");
if (property.getDescription() != null) {
this.out.append(property.getShortDescription());
}
this.out.append(System.lineSeparator());
}

private void writeDeprecatedProperty(ConfigurationMetadataProperty property) {
Deprecation deprecation = (property.getDeprecation() != null) ? property.getDeprecation() : new Deprecation();
this.out.append("|`").append(property.getId()).append("` |");
if (deprecation.getReplacement() != null) {
this.out.append("`").append(deprecation.getReplacement()).append("`");
}
this.out.append(" |");
if (deprecation.getReason() != null) {
this.out.append(getFirstSentence(deprecation.getReason()));
}
this.out.append(System.lineSeparator());
}

private String getFirstSentence(String text) {
int dot = text.indexOf('.');
if (dot != -1) {
BreakIterator breakIterator = BreakIterator.getSentenceInstance(Locale.US);
breakIterator.setText(text);
String sentence = text.substring(breakIterator.first(), breakIterator.next()).trim();
return removeSpaceBetweenLine(sentence);
}
else {
String[] lines = text.split(System.lineSeparator());
return lines[0].trim();
}
}

private static String removeSpaceBetweenLine(String text) {
String[] lines = text.split(System.lineSeparator());
StringBuilder sb = new StringBuilder();
for (String line : lines) {
sb.append(line.trim()).append(" ");
}
return sb.toString().trim();
}

private List<Difference> sortProperties(List<Difference> properties,
Function<Difference, ConfigurationMetadataProperty> property) {
List<Difference> sorted = new ArrayList<>(properties);
sorted.sort((o1, o2) -> property.apply(o1).getId().compareTo(property.apply(o2).getId()));
return sorted;
}

private static String defaultValueToString(Object defaultValue) {
if (defaultValue instanceof Object[]) {
return Stream.of((Object[]) defaultValue).map(Object::toString).collect(Collectors.joining(", "));
}
else {
return defaultValue.toString();
}
}

@Override
public void close() {
this.out.close();
}

}
Loading

0 comments on commit 32b7b31

Please sign in to comment.