Browse Source

Check javadoc macros in adoc files

Closes gh-48298
pull/48470/head
Andy Wilkinson 1 month ago
parent
commit
710e567ba5
  1. 25
      buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java
  2. 446
      buildSrc/src/main/java/org/springframework/boot/build/antora/CheckJavadocMacros.java
  3. 62
      spring-boot-project/spring-boot-docs/build.gradle
  4. 2
      spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc
  5. 3
      spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-security.adoc
  6. 2
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc
  7. 2
      spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc

25
buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java

@ -40,12 +40,16 @@ import org.gradle.api.file.Directory; @@ -40,12 +40,16 @@ import org.gradle.api.file.Directory;
import org.gradle.api.file.FileCollection;
import org.gradle.api.logging.LogLevel;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Copy;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.TaskProvider;
import org.springframework.boot.build.antora.AntoraAsciidocAttributes;
import org.springframework.boot.build.antora.CheckJavadocMacros;
import org.springframework.boot.build.antora.GenerateAntoraPlaybook;
import org.springframework.boot.build.bom.BomExtension;
import org.springframework.boot.build.bom.ResolvedBom;
@ -97,6 +101,27 @@ public class AntoraConventions { @@ -97,6 +101,27 @@ public class AntoraConventions {
(antoraTask) -> configureAntoraTask(project, antoraTask, npmInstallTask, generateAntoraPlaybookTask));
project.getExtensions()
.configure(NodeExtension.class, (nodeExtension) -> configureNodeExtension(project, nodeExtension));
TaskProvider<CheckJavadocMacros> checkAntoraJavadocMacros = tasks.register("checkAntoraJavadocMacros",
CheckJavadocMacros.class, (task) -> {
task.setSource(project.files(ANTORA_SOURCE_DIR));
task.getOutputDirectory().set(project.getLayout().getBuildDirectory().dir(task.getName()));
});
project.getPlugins().withType(JavaPlugin.class, (java) -> {
String runtimeClasspathConfigurationName = project.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
.getRuntimeClasspathConfigurationName();
Configuration javadocMacros = project.getConfigurations().create("javadocMacros", (configuration) -> {
configuration.extendsFrom(project.getConfigurations().getByName(runtimeClasspathConfigurationName));
configuration.setDescription(
"Dependencies referenced in javadoc macros. Extends from " + runtimeClasspathConfigurationName);
configuration.setCanBeResolved(true);
configuration.setCanBeDeclared(true);
configuration.setCanBeConsumed(false);
});
checkAntoraJavadocMacros.configure((macrosTask) -> macrosTask.setClasspath(javadocMacros));
});
}
private void configureGenerateAntoraPlaybookTask(Project project,

446
buildSrc/src/main/java/org/springframework/boot/build/antora/CheckJavadocMacros.java

@ -0,0 +1,446 @@ @@ -0,0 +1,446 @@
/*
* Copyright 2025 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.antora;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.zip.ZipEntry;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.VerificationException;
import org.springframework.asm.ClassReader;
import org.springframework.asm.ClassVisitor;
import org.springframework.asm.FieldVisitor;
import org.springframework.asm.MethodVisitor;
import org.springframework.asm.SpringAsmInfo;
import org.springframework.asm.Type;
import org.springframework.util.function.ThrowingConsumer;
/**
* A task to check {@code javadoc:[]} macros in Antora source files.
*
* @author Andy Wilkinson
*/
public abstract class CheckJavadocMacros extends DefaultTask {
private static final Pattern JAVADOC_MACRO_PATTERN = Pattern.compile("javadoc:(.*?)\\[(.*?)\\]");
private final Path projectRoot;
private FileCollection source;
private FileCollection classpath;
public CheckJavadocMacros() {
this.projectRoot = getProject().getRootDir().toPath();
}
@InputFiles
public FileCollection getSource() {
return this.source;
}
public void setSource(FileCollection source) {
this.source = source;
}
@Optional
@Classpath
public FileCollection getClasspath() {
return this.classpath;
}
public void setClasspath(FileCollection classpath) {
this.classpath = classpath;
}
@OutputDirectory
public abstract DirectoryProperty getOutputDirectory();
@TaskAction
void checkJavadocMacros() {
Set<String> availableClasses = indexClasspath();
List<String> problems = new ArrayList<>();
this.source.getAsFileTree()
.filter((file) -> file.getName().endsWith(".adoc"))
.forEach((file) -> problems.addAll(checkJavadocMacros(file, availableClasses)));
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
writeReport(problems, outputFile);
if (!problems.isEmpty()) {
throw new VerificationException("Javadoc macro check failed. See '%s' for details".formatted(outputFile));
}
}
private Set<String> indexClasspath() {
Set<String> availableClasses = StreamSupport.stream(this.classpath.spliterator(), false).flatMap((root) -> {
if (root.isFile()) {
try (JarFile jar = new JarFile(root)) {
return jar.stream()
.map(JarEntry::getName)
.filter((entryName) -> entryName.endsWith(".class"))
.map((className) -> {
if (className.startsWith("META-INF/versions/")) {
className = className.substring("META-INF/versions/".length());
}
className = className.substring(0, className.length() - ".class".length());
className = className.replace('/', '.');
return className;
})
.toList()
.stream();
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
return Stream.empty();
}).collect(Collectors.toSet());
return availableClasses;
}
private List<String> checkJavadocMacros(File adocFile, Set<String> availableClasses) {
List<String> problems = new ArrayList<>();
List<JavadocMacro> macros = JavadocMacro.parse(adocFile);
for (JavadocMacro macro : macros) {
if (!classIsAvailable(macro.className.name, availableClasses)) {
problems.add(this.projectRoot.relativize(macro.className.origin.file.toPath()) + ":"
+ macro.className.origin.line + ":" + macro.className.origin.column + ": class "
+ macro.className.name + " does not exist.");
}
else {
JavadocAnchor anchor = macro.anchor;
if (anchor != null) {
if (anchor instanceof MethodAnchor methodAnchor) {
MethodMatcher methodMatcher = new MethodMatcher(methodAnchor);
inputStreamOf(macro.className.name, (stream) -> {
ClassReader reader = new ClassReader(stream);
reader.accept(methodMatcher, 0);
});
if (!methodMatcher.matched) {
problems.add(this.projectRoot.relativize(macro.anchor.origin.file.toPath()) + ":"
+ macro.anchor.origin.line + ":" + methodAnchor.origin().column + ": method "
+ methodAnchor + " does not exist");
}
}
else if (anchor instanceof FieldAnchor fieldAnchor) {
FieldMatcher fieldMatcher = new FieldMatcher(fieldAnchor);
inputStreamOf(macro.className.name, (stream) -> {
ClassReader reader = new ClassReader(stream);
reader.accept(fieldMatcher, 0);
});
if (!fieldMatcher.matched) {
problems.add(this.projectRoot.relativize(macro.anchor.origin.file.toPath()) + ":"
+ macro.anchor.origin.line + ":" + fieldAnchor.origin().column + ": field "
+ fieldAnchor.name + " does not exist");
}
}
}
}
}
return problems;
}
private boolean classIsAvailable(String className, Set<String> availableClasses) {
if (availableClasses.contains(className)) {
return true;
}
if (className.startsWith("java.") || className.startsWith("javax.")) {
return jdkResourceForClass(className) != null;
}
return false;
}
private URL jdkResourceForClass(String className) {
return getClass().getClassLoader().getResource(className.replace(".", "/") + ".class");
}
private void inputStreamOf(String className, ThrowingConsumer<InputStream> streamHandler) {
for (File root : this.classpath) {
if (root.isFile()) {
try (JarFile jar = new JarFile(root)) {
ZipEntry entry = jar.getEntry(className.replace(".", "/") + ".class");
if (entry != null) {
try (InputStream stream = jar.getInputStream(entry)) {
streamHandler.accept(stream);
}
return;
}
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
URL resource = jdkResourceForClass(className);
if (resource != null) {
try (InputStream stream = resource.openStream()) {
streamHandler.accept(stream);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
private void writeReport(List<String> problems, File outputFile) {
outputFile.getParentFile().mkdirs();
StringBuilder report = new StringBuilder();
if (!problems.isEmpty()) {
if (problems.size() == 1) {
report.append("Found 1 javadoc macro problem:%n".formatted());
}
else {
report.append("Found %d javadoc macro problems:%n".formatted(problems.size()));
}
problems.forEach((problem) -> report.append("%s%n".formatted(problem)));
}
try {
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
private static final class JavadocMacro {
private final ClassName className;
private final JavadocAnchor anchor;
private JavadocMacro(ClassName className, JavadocAnchor anchor) {
this.className = className;
this.anchor = anchor;
}
private static List<JavadocMacro> parse(File adocFile) {
List<JavadocMacro> macros = new ArrayList<>();
try {
Path adocFilePath = adocFile.toPath();
List<String> lines = Files.readAllLines(adocFilePath);
for (int i = 0; i < lines.size(); i++) {
Matcher matcher = JAVADOC_MACRO_PATTERN.matcher(lines.get(i));
while (matcher.find()) {
Origin classNameOrigin = new Origin(adocFile, i + 1, matcher.start(1) + 1);
String target = matcher.group(1);
String className = target;
int endOfUrlAttribute = className.indexOf("}/");
if (endOfUrlAttribute != -1) {
className = className.substring(endOfUrlAttribute + 2);
}
JavadocAnchor anchor = null;
int anchorIndex = className.indexOf("#");
if (anchorIndex != -1) {
anchor = JavadocAnchor.of(className.substring(anchorIndex + 1), new Origin(adocFile,
classNameOrigin.line(), classNameOrigin.column + anchorIndex + 1));
className = className.substring(0, anchorIndex);
}
macros.add(new JavadocMacro(new ClassName(classNameOrigin, className), anchor));
}
}
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
return macros;
}
}
private static final class ClassName {
private final Origin origin;
private final String name;
private ClassName(Origin origin, String name) {
this.origin = origin;
this.name = name;
}
}
private record Origin(File file, int line, int column) {
}
private abstract static class JavadocAnchor {
private final Origin origin;
protected JavadocAnchor(Origin origin) {
this.origin = origin;
}
Origin origin() {
return this.origin;
}
private static JavadocAnchor of(String anchor, Origin origin) {
JavadocAnchor javadocAnchor = WellKnownAnchor.of(anchor, origin);
if (javadocAnchor == null) {
javadocAnchor = MethodAnchor.of(anchor, origin);
}
if (javadocAnchor == null) {
javadocAnchor = FieldAnchor.of(anchor, origin);
}
return javadocAnchor;
}
}
private static final class WellKnownAnchor extends JavadocAnchor {
private WellKnownAnchor(Origin origin) {
super(origin);
}
private static WellKnownAnchor of(String anchor, Origin origin) {
if (anchor.equals("enum-constant-summary")) {
return new WellKnownAnchor(origin);
}
return null;
}
}
private static final class MethodAnchor extends JavadocAnchor {
private final String name;
private final List<String> arguments;
private MethodAnchor(String name, List<String> arguments, Origin origin) {
super(origin);
this.name = name;
this.arguments = arguments;
}
@Override
public String toString() {
return this.name + "(" + String.join(", ", this.arguments + ")");
}
static MethodAnchor of(String anchor, Origin origin) {
if (!anchor.contains("(")) {
return null;
}
int openingIndex = anchor.indexOf('(');
String name = anchor.substring(0, openingIndex);
List<String> arguments = Stream.of(anchor.substring(openingIndex + 1, anchor.length() - 1).split(","))
.map(String::trim)
.map((argument) -> argument.endsWith("...") ? argument.replace("...", "[]") : argument)
.toList();
return new MethodAnchor(name, arguments, origin);
}
}
private static final class FieldAnchor extends JavadocAnchor {
private final String name;
private FieldAnchor(String name, Origin origin) {
super(origin);
this.name = name;
}
static FieldAnchor of(String anchor, Origin origin) {
return new FieldAnchor(anchor, origin);
}
}
private static final class MethodMatcher extends ClassVisitor {
private final MethodAnchor methodAnchor;
private boolean matched = false;
private MethodMatcher(MethodAnchor methodAnchor) {
super(SpringAsmInfo.ASM_VERSION);
this.methodAnchor = methodAnchor;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
if (!this.matched && name.equals(this.methodAnchor.name)) {
Type type = Type.getType(descriptor);
if (type.getArgumentCount() == this.methodAnchor.arguments.size()) {
List<String> argumentTypeNames = Arrays.asList(type.getArgumentTypes())
.stream()
.map(Type::getClassName)
.toList();
if (argumentTypeNames.equals(this.methodAnchor.arguments)) {
this.matched = true;
}
}
}
return null;
}
}
private static final class FieldMatcher extends ClassVisitor {
private final FieldAnchor fieldAnchor;
private boolean matched = false;
private FieldMatcher(FieldAnchor fieldAnchor) {
super(SpringAsmInfo.ASM_VERSION);
this.fieldAnchor = fieldAnchor;
}
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
if (!this.matched && name.equals(this.fieldAnchor.name)) {
this.matched = true;
}
return null;
}
}
}

62
spring-boot-project/spring-boot-docs/build.gradle

@ -181,6 +181,68 @@ dependencies { @@ -181,6 +181,68 @@ dependencies {
implementation("org.junit.jupiter:junit-jupiter")
implementation("org.yaml:snakeyaml")
javadocMacros(project(":spring-boot-project:spring-boot-tools:spring-boot-loader"))
javadocMacros("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
javadocMacros("com.google.code.gson:gson")
javadocMacros("com.h2database:h2")
javadocMacros("com.hazelcast:hazelcast")
javadocMacros("com.hazelcast:hazelcast-spring")
javadocMacros("com.redis:testcontainers-redis")
javadocMacros("io.lettuce:lettuce-core")
javadocMacros("io.micrometer:micrometer-registry-new-relic")
javadocMacros("io.opentelemetry:opentelemetry-sdk-logs")
javadocMacros("io.opentelemetry:opentelemetry-sdk-metrics")
javadocMacros("io.opentelemetry:opentelemetry-sdk-trace")
javadocMacros("io.prometheus:prometheus-metrics-tracer-common")
javadocMacros("io.prometheus:simpleclient_tracer_common")
javadocMacros("io.rsocket:rsocket-core")
javadocMacros("jakarta.json.bind:jakarta.json.bind-api")
javadocMacros("jakarta.mail:jakarta.mail-api")
javadocMacros("jakarta.websocket:jakarta.websocket-api")
javadocMacros("org.apache.logging.log4j:log4j-core")
javadocMacros("org.apache.activemq:artemis-jakarta-server")
javadocMacros("org.eclipse.jetty:jetty-client")
javadocMacros("org.eclipse.jetty:jetty-server")
javadocMacros("org.elasticsearch.client:elasticsearch-rest-client-sniffer")
javadocMacros("org.flywaydb:flyway-core")
javadocMacros("org.infinispan:infinispan-core")
javadocMacros("org.liquibase:liquibase-core")
javadocMacros("org.messaginghub:pooled-jms")
javadocMacros("org.postgresql:postgresql")
javadocMacros("org.projectlombok:lombok")
javadocMacros("org.seleniumhq.selenium:selenium-api")
javadocMacros("org.springframework.amqp:spring-rabbit-stream")
javadocMacros("org.springframework.data:spring-data-jdbc")
javadocMacros("org.springframework.data:spring-data-rest-webmvc")
javadocMacros("org.springframework.hateoas:spring-hateoas")
javadocMacros("org.springframework.integration:spring-integration-core")
javadocMacros("org.springframework.integration:spring-integration-rsocket")
javadocMacros("org.springframework.security:spring-security-oauth2-authorization-server")
javadocMacros("org.springframework.security:spring-security-oauth2-jose")
javadocMacros("org.springframework.security:spring-security-oauth2-resource-server")
javadocMacros("org.springframework.security:spring-security-saml2-service-provider") {
exclude group: "org.opensaml"
}
javadocMacros("org.springframework.session:spring-session-core")
javadocMacros("org.testcontainers:activemq")
javadocMacros("org.testcontainers:cassandra")
javadocMacros("org.testcontainers:clickhouse")
javadocMacros("org.testcontainers:couchbase")
javadocMacros("org.testcontainers:elasticsearch")
javadocMacros("org.testcontainers:grafana")
javadocMacros("org.testcontainers:jdbc")
javadocMacros("org.testcontainers:kafka")
javadocMacros("org.testcontainers:mariadb")
javadocMacros("org.testcontainers:mssqlserver")
javadocMacros("org.testcontainers:mysql")
javadocMacros("org.testcontainers:oracle-free")
javadocMacros("org.testcontainers:oracle-xe")
javadocMacros("org.testcontainers:postgresql")
javadocMacros("org.testcontainers:pulsar")
javadocMacros("org.testcontainers:rabbitmq")
javadocMacros("org.testcontainers:redpanda")
javadocMacros("org.thymeleaf:thymeleaf-spring6")
remoteSpringApplicationExample(platform(project(":spring-boot-project:spring-boot-dependencies")))
remoteSpringApplicationExample(project(":spring-boot-project:spring-boot-devtools"))
remoteSpringApplicationExample(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-logging"))

2
spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc

@ -110,7 +110,7 @@ This can be useful when you want to re-order or remove some of the converters th @@ -110,7 +110,7 @@ This can be useful when you want to re-order or remove some of the converters th
=== MessageCodesResolver
Spring MVC has a strategy for generating error codes for rendering error messages from binding errors: javadoc:org.springframework.validation.MessageCodesResolver[].
If you set the configprop:spring.mvc.message-codes-resolver-format[] property `PREFIX_ERROR_CODE` or `POSTFIX_ERROR_CODE`, Spring Boot creates one for you (see the enumeration in javadoc:org.springframework.validation.DefaultMessageCodesResolver#Format[]).
If you set the configprop:spring.mvc.message-codes-resolver-format[] property `PREFIX_ERROR_CODE` or `POSTFIX_ERROR_CODE`, Spring Boot creates one for you (see the enumeration in javadoc:org.springframework.validation.DefaultMessageCodesResolver$Format[]).

3
spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/web/spring-security.adoc

@ -61,8 +61,9 @@ javadoc:org.springframework.boot.autoconfigure.security.servlet.PathRequest[] ca @@ -61,8 +61,9 @@ javadoc:org.springframework.boot.autoconfigure.security.servlet.PathRequest[] ca
Similar to Spring MVC applications, you can secure your WebFlux applications by adding the `spring-boot-starter-security` dependency.
The default security configuration is implemented in javadoc:org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration[] and javadoc:org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration[].
javadoc:org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration[] imports `WebFluxSecurityConfiguration` for web security and javadoc:org.springframework.boot.autoconfigure.security.reactive.UserDetailsServiceAutoConfiguration[] for authentication.
In addition to reactive web applications, the latter is also auto-configured when RSocket is in use.
javadoc:org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration[] imports `WebFluxSecurityConfiguration` for web security.
javadoc:org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration[] auto-configures authentication.
To completely switch off the default web application security configuration, including Actuator security, add a bean of type javadoc:org.springframework.security.web.server.WebFilterChainProxy[] (doing so does not disable the javadoc:org.springframework.security.core.userdetails.ReactiveUserDetailsService[] configuration).
To also switch off the javadoc:org.springframework.security.core.userdetails.ReactiveUserDetailsService[] configuration, add a bean of type javadoc:org.springframework.security.core.userdetails.ReactiveUserDetailsService[] or javadoc:org.springframework.security.authentication.ReactiveAuthenticationManager[].

2
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc

@ -140,7 +140,7 @@ Refer to documentation of the builder being used to determine the image OS and a @@ -140,7 +140,7 @@ Refer to documentation of the builder being used to determine the image OS and a
| `imageName`
| `--imageName`
| javadoc:org.springframework.boot.buildpack.platform.docker.type.ImageReference#of-java.lang.String-[Image name] for the generated image.
| javadoc:org.springframework.boot.buildpack.platform.docker.type.ImageReference#of(java.lang.String)[Image name] for the generated image.
| `docker.io/library/${project.name}:${project.version}`
| `pullPolicy`

2
spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc

@ -156,7 +156,7 @@ Refer to documentation of the builder being used to determine the image OS and a @@ -156,7 +156,7 @@ Refer to documentation of the builder being used to determine the image OS and a
| `name` +
(`spring-boot.build-image.imageName`)
| javadoc:org.springframework.boot.buildpack.platform.docker.type.ImageName#of-java.lang.String-[Image name] for the generated image.
| javadoc:org.springframework.boot.buildpack.platform.docker.type.ImageName#of(java.lang.String)[Image name] for the generated image.
| `docker.io/library/` +
`${project.artifactId}:${project.version}`

Loading…
Cancel
Save