Add external links to `spring-boot-dependencies`

Update the BOM `Library` model to support external links that we
can use in documentation and the release process.

An additional `checkLinks` task has also been added to verify
returned HTTP status codes.

Closes gh-39779

Co-authored-by: Andy Wilkinson <andy.wilkinson@broadcom.com>
This commit is contained in:
Phillip Webb 2024-02-27 23:16:17 -08:00
parent e44ec27fd6
commit 75c7bed6c6
7 changed files with 666 additions and 6 deletions

View File

@ -47,6 +47,7 @@ dependencies {
implementation("commons-codec:commons-codec:${versions.commonsCodec}")
implementation("de.undercouch.download:de.undercouch.download.gradle.plugin:5.5.0")
implementation("io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}")
implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
implementation("org.apache.maven:maven-embedder:${versions.maven}")
implementation("org.asciidoctor:asciidoctor-gradle-jvm:3.3.2")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}")

View File

@ -26,6 +26,7 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilderFactory;
@ -66,11 +67,14 @@ import org.springframework.boot.build.bom.Library.VersionAlignment;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
import org.springframework.boot.build.mavenplugin.MavenExec;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
/**
* DSL extensions for {@link BomPlugin}.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class BomExtension {
@ -119,7 +123,8 @@ public class BomExtension {
this.project, this.libraries, libraryHandler.groups)
: null;
addLibrary(new Library(name, libraryHandler.calendarName, libraryVersion, libraryHandler.groups,
libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots, versionAlignment));
libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots, versionAlignment,
libraryHandler.links));
}
public void effectiveBomArtifact() {
@ -227,6 +232,8 @@ public class BomExtension {
private AlignWithVersionHandler alignWithVersion;
private final Map<String, Function<LibraryVersion, String>> links = new HashMap<>();
@Inject
public LibraryHandler(String version) {
this.version = version;
@ -263,6 +270,12 @@ public class BomExtension {
action.execute(this.alignWithVersion);
}
public void links(Action<LinksHandler> action) {
LinksHandler handler = new LinksHandler();
action.execute(handler);
this.links.putAll(handler.links);
}
public static class ProhibitedHandler {
private String reason;
@ -398,6 +411,67 @@ public class BomExtension {
}
public static class LinksHandler {
private final Map<String, Function<LibraryVersion, String>> links = new HashMap<>();
public void site(String linkTemplate) {
site(asFactory(linkTemplate));
}
public void site(Function<LibraryVersion, String> linkFactory) {
add("site", linkFactory);
}
public void github(String linkTemplate) {
github(asFactory(linkTemplate));
}
public void github(Function<LibraryVersion, String> linkFactory) {
add("github", linkFactory);
}
public void javadoc(String linkTemplate) {
javadoc(asFactory(linkTemplate));
}
public void javadoc(Function<LibraryVersion, String> linkFactory) {
add("javadoc", linkFactory);
}
public void reference(String linkTemplate) {
reference(asFactory(linkTemplate));
}
public void reference(Function<LibraryVersion, String> linkFactory) {
add("reference", linkFactory);
}
public void releaseNotes(String linkTemplate) {
releaseNotes(asFactory(linkTemplate));
}
public void releaseNotes(Function<LibraryVersion, String> linkFactory) {
add("releaseNotes", linkFactory);
}
public void add(String name, String linkTemplate) {
add(name, asFactory(linkTemplate));
}
public void add(String name, Function<LibraryVersion, String> linkFactory) {
this.links.put(name, linkFactory);
}
private Function<LibraryVersion, String> asFactory(String linkTemplate) {
return (version) -> {
PlaceholderResolver resolver = (name) -> "version".equals(name) ? version.toString() : null;
return new PropertyPlaceholderHelper("{", "}").replacePlaceholders(linkTemplate, resolver);
};
}
}
public static class UpgradeHandler {
private UpgradePolicy upgradePolicy;

View File

@ -66,8 +66,8 @@ public class BomPlugin implements Plugin<Project> {
project.getTasks().named("check").configure((check) -> check.dependsOn(checkBom));
project.getTasks().create("bomrUpgrade", UpgradeBom.class, bom);
project.getTasks().create("moveToSnapshots", MoveToSnapshots.class, bom);
project.getTasks().register("checkLinks", CheckLinks.class, bom);
new PublishingCustomizer(project, bom).customize();
}
private void createApiEnforcedConfiguration(Project project) {

View File

@ -0,0 +1,85 @@
/*
* Copyright 2024-2024 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.build.bom;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import javax.inject.Inject;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.TaskAction;
import org.gradle.internal.impldep.org.apache.http.client.config.CookieSpecs;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;
/**
* Task to check that links are working.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class CheckLinks extends DefaultTask {
private final BomExtension bom;
@Inject
public CheckLinks(BomExtension bom) {
this.bom = bom;
}
@TaskAction
void releaseNotes() {
RequestConfig config = RequestConfig.custom().setCookieSpec(CookieSpecs.IGNORE_COOKIES).build();
CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(config).build();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
RestTemplate restTemplate = new RestTemplate(requestFactory);
restTemplate.setErrorHandler(new IgnoringErrorHandler());
for (Library library : this.bom.getLibraries()) {
library.getLinks().forEach((name, link) -> {
URI uri;
try {
uri = new URI(link);
ResponseEntity<String> response = restTemplate.exchange(uri, HttpMethod.HEAD, null, String.class);
System.out.println("[%3d] %s - %s (%s)".formatted(response.getStatusCode().value(),
library.getName(), name, uri));
}
catch (URISyntaxException ex) {
throw new RuntimeException(ex);
}
});
}
}
static class IgnoringErrorHandler extends DefaultResponseErrorHandler {
@Override
public void handleError(ClientHttpResponse response) throws IOException {
}
}
}

View File

@ -17,6 +17,7 @@
package org.springframework.boot.build.bom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@ -24,6 +25,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Function;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.VersionRange;
@ -59,6 +62,8 @@ public class Library {
private final VersionAlignment versionAlignment;
private final Map<String, Function<LibraryVersion, String>> links;
/**
* Create a new {@code Library} with the given {@code name}, {@code version}, and
* {@code groups}.
@ -70,9 +75,11 @@ public class Library {
* @param prohibitedVersions version of the library that are prohibited
* @param considerSnapshots whether to consider snapshots
* @param versionAlignment version alignment, if any, for the library
* @param links a list of HTTP links relevant to the library
*/
public Library(String name, String calendarName, LibraryVersion version, List<Group> groups,
List<ProhibitedVersion> prohibitedVersions, boolean considerSnapshots, VersionAlignment versionAlignment) {
List<ProhibitedVersion> prohibitedVersions, boolean considerSnapshots, VersionAlignment versionAlignment,
Map<String, Function<LibraryVersion, String>> links) {
this.name = name;
this.calendarName = (calendarName != null) ? calendarName : name;
this.version = version;
@ -82,6 +89,7 @@ public class Library {
this.prohibitedVersions = prohibitedVersions;
this.considerSnapshots = considerSnapshots;
this.versionAlignment = versionAlignment;
this.links = Collections.unmodifiableMap(links);
}
public String getName() {
@ -116,6 +124,12 @@ public class Library {
return this.versionAlignment;
}
public Map<String, String> getLinks() {
Map<String, String> links = new TreeMap<>();
this.links.forEach((name, linkFactory) -> links.put(name, linkFactory.apply(this.version)));
return Collections.unmodifiableMap(links);
}
/**
* A version or range of versions that are prohibited from being used in a bom.
*/
@ -184,6 +198,44 @@ public class Library {
return this.version;
}
public int[] componentInts() {
return Arrays.stream(parts()).mapToInt(Integer::parseInt).toArray();
}
public String major() {
return parts()[0];
}
public String minor() {
return parts()[1];
}
public String patch() {
return parts()[2];
}
@Override
public String toString() {
return this.version.toString();
}
public String toString(String separator) {
return this.version.toString().replace(".", separator);
}
public String forAntora() {
String[] parts = parts();
String result = parts[0] + "." + parts[1];
if (toString().endsWith("SNAPSHOT")) {
result += "-SNAPSHOT";
}
return result;
}
private String[] parts() {
return toString().split("[\\.-]");
}
}
/**

View File

@ -21,6 +21,7 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Collections;
import java.util.Properties;
import org.junit.jupiter.api.Test;
@ -53,7 +54,7 @@ class UpgradeApplicatorTests {
FileCopyUtils.copy(new File("src/test/resources/gradle.properties"), gradleProperties);
new UpgradeApplicator(bom.toPath(), gradleProperties.toPath())
.apply(new Upgrade(new Library("ActiveMQ", null, new LibraryVersion(DependencyVersion.parse("5.15.11")),
null, null, false, null), DependencyVersion.parse("5.16")));
null, null, false, null, Collections.emptyMap()), DependencyVersion.parse("5.16")));
String bomContents = Files.readString(bom.toPath());
assertThat(bomContents).hasSize(originalContents.length() - 3);
}
@ -66,7 +67,7 @@ class UpgradeApplicatorTests {
FileCopyUtils.copy(new File("src/test/resources/gradle.properties"), gradleProperties);
new UpgradeApplicator(bom.toPath(), gradleProperties.toPath())
.apply(new Upgrade(new Library("Kotlin", null, new LibraryVersion(DependencyVersion.parse("1.3.70")), null,
null, false, null), DependencyVersion.parse("1.4")));
null, false, null, Collections.emptyMap()), DependencyVersion.parse("1.4")));
Properties properties = new Properties();
try (InputStream in = new FileInputStream(gradleProperties)) {
properties.load(in);

File diff suppressed because it is too large Load Diff