Skip to content

Commit

Permalink
Polish config metadata changelog generator
Browse files Browse the repository at this point in the history
  • Loading branch information
philwebb committed Jun 30, 2023
1 parent 7395420 commit 1bf334a
Show file tree
Hide file tree
Showing 17 changed files with 735 additions and 543 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ if (project.hasProperty("oldVersion") && project.hasProperty("newVersion")) {
inputs.files(prepareOldMetadata, prepareNewMetadata)
outputs.file(project.file("build/configuration-metadata-changelog.adoc"))
classpath = sourceSets.main.runtimeClasspath
mainClass = 'org.springframework.boot.configurationmetadata.changelog.ConfigurationMetadataChangelogGenerator'
mainClass = 'org.springframework.boot.configurationmetadata.changelog.ChangelogGenerator'
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")]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.util.ArrayList;
import java.util.List;

import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository;

/**
* A changelog containing differences computed from two repositories of configuration
* metadata.
*
* @param oldVersionNumber the name of the old version
* @param newVersionNumber the name of the new version
* @param differences the differences
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Phillip Webb
*/
record Changelog(String oldVersionNumber, String newVersionNumber, List<Difference> differences) {

static Changelog of(String oldVersionNumber, ConfigurationMetadataRepository oldMetadata, String newVersionNumber,
ConfigurationMetadataRepository newMetadata) {
return new Changelog(oldVersionNumber, newVersionNumber, computeDifferences(oldMetadata, newMetadata));
}

static List<Difference> computeDifferences(ConfigurationMetadataRepository oldMetadata,
ConfigurationMetadataRepository newMetadata) {
List<String> seenIds = new ArrayList<>();
List<Difference> differences = new ArrayList<>();
for (ConfigurationMetadataProperty oldProperty : oldMetadata.getAllProperties().values()) {
String id = oldProperty.getId();
seenIds.add(id);
ConfigurationMetadataProperty newProperty = newMetadata.getAllProperties().get(id);
Difference difference = Difference.compute(oldProperty, newProperty);
if (difference != null) {
differences.add(difference);
}
}
for (ConfigurationMetadataProperty newProperty : newMetadata.getAllProperties().values()) {
if ((!seenIds.contains(newProperty.getId())) && (!newProperty.isDeprecated())) {
differences.add(new Difference(DifferenceType.ADDED, null, newProperty));
}
}
return List.copyOf(differences);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.IOException;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepository;
import org.springframework.boot.configurationmetadata.ConfigurationMetadataRepositoryJsonBuilder;

/**
* Generates a configuration metadata changelog. Requires three arguments:
*
* <ol>
* <li>The path of a directory containing jar files of the old version
* <li>The path of a directory containing jar files of the new version
* <li>The path of a file to which the asciidoc changelog will be written
* </ol>
*
* The name of each directory will be used as version numbers in generated changelog.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.2.0
*/
public final class ChangelogGenerator {

private ChangelogGenerator() {
}

public static void main(String[] args) throws IOException {
generate(new File(args[0]), new File(args[1]), new File(args[2]));
}

private static void generate(File oldDir, File newDir, File out) throws IOException {
String oldVersionNumber = oldDir.getName();
ConfigurationMetadataRepository oldMetadata = buildRepository(oldDir);
String newVersionNumber = newDir.getName();
ConfigurationMetadataRepository newMetadata = buildRepository(newDir);
Changelog changelog = Changelog.of(oldVersionNumber, oldMetadata, newVersionNumber, newMetadata);
try (ChangelogWriter writer = new ChangelogWriter(out)) {
writer.write(changelog);
}
System.out.println("%nConfiguration metadata changelog written to '%s'".formatted(out));
}

static ConfigurationMetadataRepository buildRepository(File directory) {
ConfigurationMetadataRepositoryJsonBuilder builder = ConfigurationMetadataRepositoryJsonBuilder.create();
for (File file : directory.listFiles()) {
try (JarFile jarFile = new JarFile(file)) {
JarEntry metadataEntry = jarFile.getJarEntry("META-INF/spring-configuration-metadata.json");
if (metadataEntry != null) {
builder.withJsonResource(jarFile.getInputStream(metadataEntry));
}
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
return builder.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/*
* 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;
import java.io.PrintWriter;
import java.io.Writer;
import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty;
import org.springframework.boot.configurationmetadata.Deprecation;

/**
* Writes a {@link Changelog} using asciidoc markup.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Phillip Webb
*/
class ChangelogWriter implements AutoCloseable {

private static final Comparator<ConfigurationMetadataProperty> COMPARING_ID = Comparator
.comparing(ConfigurationMetadataProperty::getId);

private final PrintWriter out;

ChangelogWriter(File out) throws IOException {
this(new FileWriter(out));
}

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

void write(Changelog changelog) {
String oldVersionNumber = changelog.oldVersionNumber();
String newVersionNumber = changelog.newVersionNumber();
Map<DifferenceType, List<Difference>> differencesByType = collateByType(changelog);
write("Configuration property changes between `%s` and `%s`%n", oldVersionNumber, newVersionNumber);
write("%n%n%n== Deprecated in %s%n", newVersionNumber);
writeDeprecated(differencesByType.get(DifferenceType.DEPRECATED));
write("%n%n%n== Added in %s%n", newVersionNumber);
writeAdded(differencesByType.get(DifferenceType.ADDED));
write("%n%n%n== Removed in %s%n", newVersionNumber);
writeRemoved(differencesByType.get(DifferenceType.DELETED), differencesByType.get(DifferenceType.DEPRECATED));
}

private Map<DifferenceType, List<Difference>> collateByType(Changelog differences) {
Map<DifferenceType, List<Difference>> byType = new HashMap<>();
for (DifferenceType type : DifferenceType.values()) {
byType.put(type, new ArrayList<>());
}
for (Difference difference : differences.differences()) {
byType.get(difference.type()).add(difference);
}
return byType;
}

private void writeDeprecated(List<Difference> differences) {
List<Difference> rows = sortProperties(differences, Difference::newProperty).stream()
.filter(this::isDeprecatedInRelease)
.collect(Collectors.toList());
writeTable("| Key | Replacement | Reason", rows, this::writeDeprecated);
}

private void writeDeprecated(Difference difference) {
writeDeprecatedPropertyRow(difference.newProperty());
}

private void writeAdded(List<Difference> differences) {
List<Difference> rows = sortProperties(differences, Difference::newProperty);
writeTable("| Key | Default value | Description", rows, this::writeAdded);
}

private void writeAdded(Difference difference) {
writeRegularPropertyRow(difference.newProperty());
}

private void writeRemoved(List<Difference> deleted, List<Difference> deprecated) {
List<Difference> rows = getRemoved(deleted, deprecated);
writeTable("| Key | Replacement | Reason", rows, this::writeRemoved);
}

private List<Difference> getRemoved(List<Difference> deleted, List<Difference> deprecated) {
List<Difference> result = new ArrayList<>(deleted);
deprecated.stream().filter(Predicate.not(this::isDeprecatedInRelease)).forEach(result::remove);
return sortProperties(result,
(difference) -> getFirstNonNull(difference, Difference::oldProperty, Difference::newProperty));
}

private void writeRemoved(Difference difference) {
writeDeprecatedPropertyRow(getFirstNonNull(difference, Difference::newProperty, Difference::oldProperty));
}

private List<Difference> sortProperties(List<Difference> differences,
Function<Difference, ConfigurationMetadataProperty> extractor) {
return differences.stream().sorted(Comparator.comparing(extractor, COMPARING_ID)).toList();
}

@SafeVarargs
@SuppressWarnings("varargs")
private <T, P> P getFirstNonNull(T t, Function<T, P>... extractors) {
return Stream.of(extractors)
.map((extractor) -> extractor.apply(t))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}

private void writeTable(String header, List<Difference> rows, Consumer<Difference> action) {
if (rows.isEmpty()) {
write("_None_.%n");
}
else {
writeTableBreak();
write(header + "%n%n");
for (Iterator<Difference> iterator = rows.iterator(); iterator.hasNext();) {
action.accept(iterator.next());
write((!iterator.hasNext()) ? null : "%n");
}
writeTableBreak();
}
}

private void writeTableBreak() {
write("|======================%n");
}

private void writeRegularPropertyRow(ConfigurationMetadataProperty property) {
writeCell(monospace(property.getId()));
writeCell(monospace(asString(property.getDefaultValue())));
writeCell(property.getShortDescription());
}

private void writeDeprecatedPropertyRow(ConfigurationMetadataProperty property) {
Deprecation deprecation = (property.getDeprecation() != null) ? property.getDeprecation() : new Deprecation();
writeCell(monospace(property.getId()));
writeCell(monospace(deprecation.getReplacement()));
writeCell(getFirstSentence(deprecation.getReason()));
}

private String getFirstSentence(String text) {
if (text == null) {
return null;
}
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);
}
String[] lines = text.split(System.lineSeparator());
return lines[0].trim();
}

private String removeSpaceBetweenLine(String text) {
String[] lines = text.split(System.lineSeparator());
return Arrays.stream(lines).map(String::trim).collect(Collectors.joining(" "));
}

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

private String monospace(String value) {
return (value != null) ? "`%s`".formatted(value) : null;
}

private void writeCell(String format, Object... args) {
write((format != null) ? "| %s%n".formatted(format) : "|%n", args);
}

private void write(String format, Object... args) {
if (format != null) {
Object[] strings = Arrays.stream(args).map(this::asString).toArray();
this.out.append(format.formatted(strings));
}
}

private String asString(Object value) {
if (value instanceof Object[] array) {
return Stream.of(array).map(this::asString).collect(Collectors.joining(", "));
}
return (value != null) ? value.toString() : null;
}

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

}
Loading

0 comments on commit 1bf334a

Please sign in to comment.