diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index b6152071cf4..2670ad8d119 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -70,6 +70,14 @@ gradlePlugin { id = "org.springframework.boot.annotation-processor" implementationClass = "org.springframework.boot.build.processors.AnnotationProcessorPlugin" } + antoraAggregatedPlugin { + id = "org.springframework.boot.antora-contributor" + implementationClass = "org.springframework.boot.build.antora.AntoraContributorPlugin" + } + antoraAggregatorPlugin { + id = "org.springframework.boot.antora-dependencies" + implementationClass = "org.springframework.boot.build.antora.AntoraDependenciesPlugin" + } architecturePlugin { id = "org.springframework.boot.architecture" implementationClass = "org.springframework.boot.build.architecture.ArchitecturePlugin" diff --git a/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java index a38d946d47e..da5da165338 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/AntoraConventions.java @@ -58,10 +58,18 @@ public class AntoraConventions { private static final String DEPENDENCIES_PATH = ":spring-boot-project:spring-boot-dependencies"; - private static final String ANTORA_SOURCE_DIR = "src/docs/antora"; - private static final List NAV_FILES = List.of("nav.adoc", "local-nav.adoc"); + /** + * Default Antora source directory. + */ + public static final String ANTORA_SOURCE_DIR = "src/docs/antora"; + + /** + * Name of the {@link GenerateAntoraPlaybook} task. + */ + public static final String GENERATE_ANTORA_PLAYBOOK_TASK_NAME = "generateAntoraPlaybook"; + void apply(Project project) { project.getPlugins().withType(AntoraPlugin.class, (antoraPlugin) -> apply(project, antoraPlugin)); } @@ -70,7 +78,7 @@ public class AntoraConventions { ExtractVersionConstraints dependencyVersionsTask = addDependencyVersionsTask(project); project.getPlugins().apply(GenerateAntoraYmlPlugin.class); TaskContainer tasks = project.getTasks(); - GenerateAntoraPlaybook generateAntoraPlaybookTask = tasks.create("generateAntoraPlaybook", + GenerateAntoraPlaybook generateAntoraPlaybookTask = tasks.create(GENERATE_ANTORA_PLAYBOOK_TASK_NAME, GenerateAntoraPlaybook.class); configureGenerateAntoraPlaybookTask(project, generateAntoraPlaybookTask); Copy copyAntoraPackageJsonTask = tasks.create("copyAntoraPackageJson", Copy.class); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/AggregateContentContribution.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/AggregateContentContribution.java new file mode 100644 index 00000000000..d29b398f0ef --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/AggregateContentContribution.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-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.antora; + +import org.gradle.api.Project; + +/** + * A contribution of aggregate content. + * + * @author Andy Wilkinson + */ +class AggregateContentContribution extends ConsumableContentContribution { + + protected AggregateContentContribution(Project project, String name) { + super(project, "aggregate", name); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraContributorPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraContributorPlugin.java new file mode 100644 index 00000000000..58bfb674468 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraContributorPlugin.java @@ -0,0 +1,85 @@ +/* + * Copyright 2012-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.antora; + +import javax.inject.Inject; + +import org.antora.gradle.AntoraPlugin; +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.file.CopySpec; + +/** + * {@link Plugin} for a project that contributes to Antora-based documentation that is + * {@link AntoraDependenciesPlugin depended upon} by another project. + * + * @author Andy Wilkinson + */ +public class AntoraContributorPlugin implements Plugin { + + @Override + public void apply(Project project) { + project.getPlugins().apply(AntoraPlugin.class); + NamedDomainObjectContainer antoraContributions = project.getObjects() + .domainObjectContainer(Contribution.class, + (name) -> project.getObjects().newInstance(Contribution.class, name, project)); + project.getExtensions().add("antoraContributions", antoraContributions); + } + + public static class Contribution { + + private final String name; + + private final Project project; + + @Inject + public Contribution(String name, Project project) { + this.name = name; + this.project = project; + } + + public String getName() { + return this.name; + } + + public void source() { + new SourceContribution(this.project, this.name).produce(); + } + + public void catalogContent(Action action) { + CopySpec copySpec = this.project.copySpec(); + action.execute(copySpec); + new CatalogContentContribution(this.project, this.name).produceFrom(copySpec); + } + + public void aggregateContent(Action action) { + CopySpec copySpec = this.project.copySpec(); + action.execute(copySpec); + new AggregateContentContribution(this.project, this.name).produceFrom(copySpec); + } + + public void localAggregateContent(Action action) { + CopySpec copySpec = this.project.copySpec(); + action.execute(copySpec); + new LocalAggregateContentContribution(this.project, this.name).produceFrom(copySpec); + } + + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraDependenciesPlugin.java new file mode 100644 index 00000000000..cbb0f3b250f --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraDependenciesPlugin.java @@ -0,0 +1,80 @@ +/* + * Copyright 2012-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.antora; + +import javax.inject.Inject; + +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.Plugin; +import org.gradle.api.Project; + +/** + * {@link Plugin} for a project that depends on {@link AntoraContributorPlugin + * contributed} Antora-based documentation. + * + * @author Andy Wilkinson + */ +public class AntoraDependenciesPlugin implements Plugin { + + @Override + public void apply(Project project) { + NamedDomainObjectContainer antoraDependencies = project.getObjects() + .domainObjectContainer(AntoraDependency.class); + project.getExtensions().add("antoraDependencies", antoraDependencies); + } + + public static class AntoraDependency { + + private final String name; + + private final Project project; + + private String path; + + @Inject + public AntoraDependency(String name, Project project) { + this.name = name; + this.project = project; + } + + public String getName() { + return this.name; + } + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + this.path = path; + } + + public void catalogContent() { + new CatalogContentContribution(this.project, this.name).consumeFrom(this.path); + } + + public void aggregateContent() { + new AggregateContentContribution(this.project, this.name).consumeFrom(this.path); + } + + public void source() { + new SourceContribution(this.project, this.name).consumeFrom(this.path); + } + + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/CatalogContentContribution.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/CatalogContentContribution.java new file mode 100644 index 00000000000..d8861335d3d --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/CatalogContentContribution.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-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.antora; + +import org.gradle.api.Project; + +/** + * A contribution of catalog content. + * + * @author Andy Wilkinson + */ +class CatalogContentContribution extends ConsumableContentContribution { + + CatalogContentContribution(Project project, String name) { + super(project, "catalog", name); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/ConsumableContentContribution.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/ConsumableContentContribution.java new file mode 100644 index 00000000000..8b2b63a638f --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/ConsumableContentContribution.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-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.antora; + +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.file.CopySpec; +import org.gradle.api.file.Directory; +import org.gradle.api.file.RegularFile; +import org.gradle.api.provider.Provider; +import org.gradle.api.publish.PublishingExtension; +import org.gradle.api.publish.maven.MavenPublication; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; + +/** + * A contribution of content to Antora that can be consumed by other projects. + * + * @author Andy Wilkinson + */ +class ConsumableContentContribution extends ContentContribution { + + protected ConsumableContentContribution(Project project, String type, String name) { + super(project, name, type); + } + + @Override + void produceFrom(CopySpec copySpec) { + TaskProvider producer = super.configureProduction(copySpec); + Configuration configuration = createConfiguration(getName(), + "Configuration for %s Antora %s content artifacts."); + configuration.setCanBeConsumed(true); + configuration.setCanBeResolved(false); + getProject().getArtifacts().add(configuration.getName(), producer); + } + + void consumeFrom(String path) { + Configuration configuration = createConfiguration(getName(), "Configuration for %s Antora %s content."); + configuration.setCanBeConsumed(false); + configuration.setCanBeResolved(true); + DependencyHandler dependencies = getProject().getDependencies(); + dependencies.add(configuration.getName(), + getProject().provider(() -> projectDependency(path, configuration.getName()))); + Provider outputDirectory = outputDirectory("content", getName()); + TaskContainer tasks = getProject().getTasks(); + TaskProvider copyAntoraContent = tasks.register(taskName("copy", "%s", configuration.getName()), + CopyAntoraContent.class, (task) -> configureCopyContent(task, path, configuration, outputDirectory)); + configureAntora(addInputFrom(copyAntoraContent, configuration.getName())); + configurePlaybookGeneration(this::addToZipContentsCollectorDependencies); + getProject().getExtensions() + .getByType(PublishingExtension.class) + .getPublications() + .withType(MavenPublication.class) + .configureEach((mavenPublication) -> addPublishedMavenArtifact(mavenPublication, copyAntoraContent)); + } + + private void configureCopyContent(CopyAntoraContent task, String path, Configuration configuration, + Provider outputDirectory) { + task.setDescription( + "Syncs the %s Antora %s content from %s.".formatted(getName(), toDescription(getType()), path)); + task.setSource(configuration); + task.getOutputFile().set(outputDirectory.map(this::getContentZipFile)); + } + + private void addToZipContentsCollectorDependencies(GenerateAntoraPlaybook task) { + task.getAntoraExtensions().getZipContentsCollector().getDependencies().add(getName()); + } + + private void addPublishedMavenArtifact(MavenPublication mavenPublication, TaskProvider copyAntoraContent) { + if ("maven".equals(mavenPublication.getName())) { + String classifier = "%s-%s-content".formatted(getName(), getType()); + mavenPublication.artifact(copyAntoraContent, (mavenArtifact) -> mavenArtifact.setClassifier(classifier)); + } + } + + private RegularFile getContentZipFile(Directory dir) { + Object version = getProject().getVersion(); + return dir.file("spring-boot-docs-%s-%s-%s-content.zip".formatted(version, getName(), getType())); + } + + private static String toDescription(String input) { + return input.replace("-", " "); + } + + private Configuration createConfiguration(String name, String description) { + return getProject().getConfigurations() + .create(configurationName(name, "Antora%sContent", getType()), + (configuration) -> configuration.setDescription(description.formatted(getName(), getType()))); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/ContentContribution.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/ContentContribution.java new file mode 100644 index 00000000000..a9ba63c6ec4 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/ContentContribution.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-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.antora; + +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.file.CopySpec; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.Zip; + +/** + * A contribution of content to Antora. + * + * @author Andy Wilkinson + */ +abstract class ContentContribution extends Contribution { + + private final String type; + + protected ContentContribution(Project project, String name, String type) { + super(project, name); + this.type = type; + } + + protected String getType() { + return this.type; + } + + abstract void produceFrom(CopySpec copySpec); + + protected TaskProvider configureProduction(CopySpec copySpec) { + TaskContainer tasks = getProject().getTasks(); + TaskProvider zipContent = tasks.register(taskName("zip", "%sAntora%sContent", getName(), this.type), + Zip.class, (zip) -> { + zip.getDestinationDirectory() + .set(getProject().getLayout().getBuildDirectory().dir("generated/docs/antora-content")); + zip.getArchiveClassifier().set("%s-%s-content".formatted(getName(), this.type)); + zip.with(copySpec); + zip.setDescription("Creates a zip archive of the %s Antora %s content.".formatted(getName(), + toDescription(this.type))); + }); + configureAntora(addInputFrom(zipContent, zipContent.getName())); + return zipContent; + } + + private static String toDescription(String input) { + return input.replace("-", " "); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/Contribution.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/Contribution.java new file mode 100644 index 00000000000..37a9d5b45c3 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/Contribution.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-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.antora; + +import java.util.Arrays; +import java.util.Map; + +import org.antora.gradle.AntoraTask; +import org.apache.commons.lang3.StringUtils; +import org.gradle.api.Action; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.file.Directory; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.TaskProvider; + +import org.springframework.boot.build.AntoraConventions; + +/** + * A contribution to Antora. + * + * @author Andy Wilkinson + */ +abstract class Contribution { + + private final Project project; + + private final String name; + + protected Contribution(Project project, String name) { + this.project = project; + this.name = name; + } + + protected Project getProject() { + return this.project; + } + + protected String getName() { + return this.name; + } + + protected Dependency projectDependency(String path, String configurationName) { + return getProject().getDependencies().project(Map.of("path", path, "configuration", configurationName)); + } + + protected Provider outputDirectory(String dependencyType, String theName) { + return getProject().getLayout() + .getBuildDirectory() + .dir("generated/docs/antora-dependencies-" + dependencyType + "/" + theName); + } + + protected String taskName(String verb, String object, String... args) { + return name(verb, object, args); + } + + protected String configurationName(String name, String type, String... args) { + return name(toCamelCase(name), type, args); + } + + protected void configurePlaybookGeneration(Action action) { + this.project.getTasks() + .named(AntoraConventions.GENERATE_ANTORA_PLAYBOOK_TASK_NAME, GenerateAntoraPlaybook.class, action); + } + + protected void configureAntora(Action action) { + this.project.getTasks().named("antora", AntoraTask.class, action); + } + + protected Action addInputFrom(TaskProvider task, String propertyName) { + return (antora) -> antora.getInputs() + .files(task) + .withPathSensitivity(PathSensitivity.RELATIVE) + .withPropertyName(propertyName); + } + + private String name(String prefix, String format, String... args) { + return prefix + format.formatted(Arrays.stream(args).map(this::toPascalCase).toArray()); + } + + private String toPascalCase(String input) { + return StringUtils.capitalize(toCamelCase(input)); + } + + private String toCamelCase(String input) { + StringBuilder output = new StringBuilder(input.length()); + boolean capitalize = false; + for (char c : input.toCharArray()) { + if (c == '-') { + capitalize = true; + } + else { + output.append(capitalize ? Character.toUpperCase(c) : c); + capitalize = false; + } + } + return output.toString(); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/CopyAntoraContent.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/CopyAntoraContent.java new file mode 100644 index 00000000000..024fc005863 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/CopyAntoraContent.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-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.antora; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import javax.inject.Inject; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +/** + * Tasks to copy Antora content. + * + * @author Andy Wilkinson + */ +public abstract class CopyAntoraContent extends DefaultTask { + + private FileCollection source; + + @Inject + public CopyAntoraContent() { + } + + @InputFiles + public FileCollection getSource() { + return this.source; + } + + public void setSource(FileCollection source) { + this.source = source; + } + + @OutputFile + public abstract RegularFileProperty getOutputFile(); + + @TaskAction + void copyAntoraContent() throws IllegalStateException, IOException { + Path source = this.source.getSingleFile().toPath(); + Path target = getOutputFile().getAsFile().get().toPath(); + Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/Extensions.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/Extensions.java index 4f38141a5d3..44c6f91fb0e 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/antora/Extensions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/Extensions.java @@ -16,7 +16,7 @@ package org.springframework.boot.build.antora; -import java.nio.file.Path; +import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -158,20 +158,24 @@ public final class Extensions { customize("version_file", versionFile); } - void locations(Path... locations) { - locations(Arrays.stream(locations).map(Path::toString).toList()); - } - - private void locations(List locations) { + void locations(List locations) { customize("locations", locations); } - void alwaysInclude(Map alwaysInclude) { + void alwaysInclude(List alwaysInclude) { if (alwaysInclude != null && !alwaysInclude.isEmpty()) { - customize("always_include", List.of(new TreeMap<>(alwaysInclude))); + customize("always_include", alwaysInclude.stream().map(AlwaysInclude::asMap).toList()); } } + record AlwaysInclude(String name, String classifier) implements Serializable { + + private Map asMap() { + return new TreeMap<>(Map.of("name", name(), "classifier", classifier())); + } + + } + } class RootComponent extends Customizer { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/GenerateAntoraPlaybook.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/GenerateAntoraPlaybook.java index 3df45f7892e..92127aced3a 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/antora/GenerateAntoraPlaybook.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/GenerateAntoraPlaybook.java @@ -26,26 +26,31 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; + +import javax.inject.Inject; import org.gradle.api.DefaultTask; import org.gradle.api.Project; -import org.gradle.api.artifacts.Configuration; -import org.gradle.api.artifacts.ProjectDependency; +import org.gradle.api.file.Directory; import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.ListProperty; -import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.SetProperty; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.TaskAction; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.Yaml; +import org.springframework.boot.build.AntoraConventions; +import org.springframework.boot.build.antora.Extensions.AntoraExtensionsConfiguration.ZipContentsCollector.AlwaysInclude; + /** * Task to generate a local Antora playbook. * @@ -53,35 +58,61 @@ import org.yaml.snakeyaml.Yaml; */ public abstract class GenerateAntoraPlaybook extends DefaultTask { - private static final String ANTORA_SOURCE_DIR = "src/docs/antora"; - private static final String GENERATED_DOCS = "build/generated/docs/"; + private final Path root; + + private final Provider playbookOutputDir; + + private final String version; + + private final AntoraExtensions antoraExtensions; + + private final AsciidocExtensions asciidocExtensions; + + private final ContentSource contentSource; + @OutputFile public abstract RegularFileProperty getOutputFile(); - @Input - public abstract Property getContentSourceConfiguration(); - - @Input - @Optional - public abstract ListProperty getXrefStubs(); - - @Input - @Optional - public abstract MapProperty getAlwaysInclude(); - - @Input - @Optional - public abstract Property getExcludeJavadocExtension(); - public GenerateAntoraPlaybook() { + this.root = toRealPath(getProject().getRootDir().toPath()); + this.antoraExtensions = getProject().getObjects().newInstance(AntoraExtensions.class, this.root); + this.asciidocExtensions = getProject().getObjects().newInstance(AsciidocExtensions.class); + this.version = getProject().getVersion().toString(); + this.playbookOutputDir = configurePlaybookOutputDir(getProject()); + this.contentSource = getProject().getObjects().newInstance(ContentSource.class, this.root); setGroup("Documentation"); setDescription("Generates an Antora playbook.yml file for local use"); getOutputFile().convention(getProject().getLayout() .getBuildDirectory() .file("generated/docs/antora-playbook/antora-playbook.yml")); - getContentSourceConfiguration().convention("antoraContent"); + this.contentSource.addStartPath(getProject() + .provider(() -> getProject().getLayout().getProjectDirectory().dir(AntoraConventions.ANTORA_SOURCE_DIR))); + } + + @Nested + public AntoraExtensions getAntoraExtensions() { + return this.antoraExtensions; + } + + @Nested + public AsciidocExtensions getAsciidocExtensions() { + return this.asciidocExtensions; + } + + @Nested + public ContentSource getContentSource() { + return this.contentSource; + } + + private Provider configurePlaybookOutputDir(Project project) { + Path siteDirectory = getProject().getLayout().getBuildDirectory().dir("site").get().getAsFile().toPath(); + return project.provider(() -> { + Path playbookDir = toRealPath(getOutputFile().get().getAsFile().toPath()).getParent(); + Path outputDir = toRealPath(siteDirectory); + return "." + File.separator + playbookDir.relativize(outputDir).toString(); + }); } @TaskAction @@ -93,26 +124,14 @@ public abstract class GenerateAntoraPlaybook extends DefaultTask { } } - @Input - final Map getData() throws IOException { + private Map getData() throws IOException { Map data = loadPlaybookTemplate(); addExtensions(data); addSources(data); addDir(data); - filterJavadocExtension(data); return data; } - @SuppressWarnings("unchecked") - private void filterJavadocExtension(Map data) { - if (getExcludeJavadocExtension().getOrElse(Boolean.FALSE)) { - Map asciidoc = (Map) data.get("asciidoc"); - List extensions = new ArrayList<>((List) asciidoc.get("extensions")); - extensions.remove("@springio/asciidoctor-extensions/javadoc-extension"); - asciidoc.put("extensions", extensions); - } - } - @SuppressWarnings("unchecked") private Map loadPlaybookTemplate() throws IOException { try (InputStream resource = getClass().getResourceAsStream("antora-playbook-template.yml")) { @@ -124,21 +143,25 @@ public abstract class GenerateAntoraPlaybook extends DefaultTask { private void addExtensions(Map data) { Map antora = (Map) data.get("antora"); antora.put("extensions", Extensions.antora((extensions) -> { - extensions.xref((xref) -> xref.stub(getXrefStubs().getOrElse(Collections.emptyList()))); + extensions.xref( + (xref) -> xref.stub(this.antoraExtensions.getXref().getStubs().getOrElse(Collections.emptyList()))); extensions.zipContentsCollector((zipContentsCollector) -> { zipContentsCollector.versionFile("gradle.properties"); - String locationName = getProject().getName() + "-${version}-${name}-${classifier}.zip"; - Path antoraContent = getRelativeProjectPath() - .resolve(GENERATED_DOCS + "antora-content/" + locationName); - Path antoraDependencies = getRelativeProjectPath() - .resolve(GENERATED_DOCS + "antora-dependencies-content/" + locationName); - zipContentsCollector.locations(antoraContent, antoraDependencies); - zipContentsCollector.alwaysInclude(getAlwaysInclude().getOrNull()); + zipContentsCollector.locations(this.antoraExtensions.getZipContentsCollector() + .getLocations() + .getOrElse(Collections.emptyList())); + zipContentsCollector + .alwaysInclude(this.antoraExtensions.getZipContentsCollector().getAlwaysInclude().getOrNull()); }); extensions.rootComponent((rootComponent) -> rootComponent.name("boot")); })); Map asciidoc = (Map) data.get("asciidoc"); - asciidoc.put("extensions", Extensions.asciidoc()); + List asciidocExtensions = Extensions.asciidoc(); + if (this.asciidocExtensions.getExcludeJavadocExtension().getOrElse(Boolean.FALSE)) { + asciidocExtensions = new ArrayList<>(asciidocExtensions); + asciidocExtensions.remove("@springio/asciidoctor-extensions/javadoc-extension"); + } + asciidoc.put("extensions", asciidocExtensions); } private void addSources(Map data) { @@ -149,34 +172,17 @@ public abstract class GenerateAntoraPlaybook extends DefaultTask { private Map createContentSource() { Map source = new LinkedHashMap<>(); Path playbookPath = getOutputFile().get().getAsFile().toPath().getParent(); - Path antoraSrc = getProjectPath(getProject()).resolve(ANTORA_SOURCE_DIR); StringBuilder url = new StringBuilder("."); - relativizeFromRootProject(playbookPath).normalize().forEach((path) -> url.append(File.separator).append("..")); + this.root.relativize(playbookPath).normalize().forEach((path) -> url.append(File.separator).append("..")); source.put("url", url.toString()); source.put("branches", "HEAD"); - source.put("version", getProject().getVersion().toString()); - Set startPaths = new LinkedHashSet<>(); - addAntoraContentStartPaths(startPaths); - startPaths.add(relativizeFromRootProject(antoraSrc).toString()); - source.put("start_paths", startPaths.stream().toList()); + source.put("version", this.version); + source.put("start_paths", this.contentSource.getStartPaths().get()); return source; } - private void addAntoraContentStartPaths(Set startPaths) { - Configuration configuration = getProject().getConfigurations().findByName("antoraContent"); - if (configuration != null) { - for (ProjectDependency dependency : configuration.getAllDependencies().withType(ProjectDependency.class)) { - Path path = dependency.getDependencyProject().getProjectDir().toPath(); - startPaths.add(relativizeFromRootProject(path).resolve(ANTORA_SOURCE_DIR).toString()); - } - } - } - private void addDir(Map data) { - Path playbookDir = toRealPath(getOutputFile().get().getAsFile().toPath()).getParent(); - Path outputDir = toRealPath( - getProject().getLayout().getBuildDirectory().dir("site").get().getAsFile().toPath()); - data.put("output", Map.of("dir", "." + File.separator + playbookDir.relativize(outputDir).toString())); + data.put("output", Map.of("dir", this.playbookOutputDir.get())); } @SuppressWarnings("unchecked") @@ -201,20 +207,7 @@ public abstract class GenerateAntoraPlaybook extends DefaultTask { return new Yaml(options); } - private Path getRelativeProjectPath() { - return relativizeFromRootProject(getProjectPath(getProject())); - } - - private Path relativizeFromRootProject(Path subPath) { - Path rootProjectPath = getProjectPath(getProject().getRootProject()); - return rootProjectPath.relativize(subPath).normalize(); - } - - private Path getProjectPath(Project project) { - return toRealPath(project.getProjectDir().toPath()); - } - - private Path toRealPath(Path path) { + private static Path toRealPath(Path path) { try { return Files.exists(path) ? path.toRealPath() : path; } @@ -223,4 +216,112 @@ public abstract class GenerateAntoraPlaybook extends DefaultTask { } } + public abstract static class AntoraExtensions { + + private final Xref xref; + + private final ZipContentsCollector zipContentsCollector; + + @Inject + public AntoraExtensions(ObjectFactory objects, Path root) { + this.xref = objects.newInstance(Xref.class); + this.zipContentsCollector = objects.newInstance(ZipContentsCollector.class, root); + } + + @Nested + public Xref getXref() { + return this.xref; + } + + @Nested + public ZipContentsCollector getZipContentsCollector() { + return this.zipContentsCollector; + } + + public abstract static class Xref { + + @Input + @Optional + public abstract ListProperty getStubs(); + + } + + public abstract static class ZipContentsCollector { + + private final Provider> locations; + + @Inject + public ZipContentsCollector(Project project, Path root) { + this.locations = configureZipContentCollectorLocations(project, root); + } + + private Provider> configureZipContentCollectorLocations(Project project, Path root) { + ListProperty locations = project.getObjects().listProperty(String.class); + Path relativeProjectPath = relativize(root, project.getProjectDir().toPath()); + String locationName = project.getName() + "-${version}-${name}-${classifier}.zip"; + locations.add(project + .provider(() -> relativeProjectPath.resolve(GENERATED_DOCS + "antora-content/" + locationName) + .toString())); + locations.addAll(getDependencies().map((dependencies) -> dependencies.stream() + .map((dependency) -> relativeProjectPath + .resolve(GENERATED_DOCS + "antora-dependencies-content/" + dependency + "/" + locationName)) + .map(Path::toString) + .toList())); + return locations; + } + + private static Path relativize(Path root, Path subPath) { + return toRealPath(root).relativize(toRealPath(subPath)).normalize(); + } + + @Input + @Optional + public abstract ListProperty getAlwaysInclude(); + + @Input + @Optional + public Provider> getLocations() { + return this.locations; + } + + @Input + @Optional + public abstract SetProperty getDependencies(); + + } + + } + + public abstract static class AsciidocExtensions { + + @Inject + public AsciidocExtensions() { + + } + + @Input + @Optional + public abstract Property getExcludeJavadocExtension(); + + } + + public abstract static class ContentSource { + + private final Path root; + + @Inject + public ContentSource(Path root) { + this.root = root; + } + + @Input + public abstract ListProperty getStartPaths(); + + void addStartPath(Provider startPath) { + getStartPaths() + .add(startPath.map((dir) -> this.root.relativize(toRealPath(dir.getAsFile().toPath())).toString())); + } + + } + } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/LocalAggregateContentContribution.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/LocalAggregateContentContribution.java new file mode 100644 index 00000000000..a3cac6242a1 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/LocalAggregateContentContribution.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-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.antora; + +import org.gradle.api.Project; +import org.gradle.api.file.CopySpec; + +import org.springframework.boot.build.antora.Extensions.AntoraExtensionsConfiguration.ZipContentsCollector.AlwaysInclude; + +/** + * A contribution of aggregate content that cannot be consumed by other projects. + * + * @author Andy Wilkinson + */ +class LocalAggregateContentContribution extends ContentContribution { + + protected LocalAggregateContentContribution(Project project, String name) { + super(project, name, "local-aggregate"); + } + + @Override + void produceFrom(CopySpec copySpec) { + super.configureProduction(copySpec); + configurePlaybookGeneration(this::addToAlwaysInclude); + } + + private void addToAlwaysInclude(GenerateAntoraPlaybook task) { + task.getAntoraExtensions() + .getZipContentsCollector() + .getAlwaysInclude() + .add(new AlwaysInclude(getName(), "local-aggregate-content")); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/SourceContribution.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/SourceContribution.java new file mode 100644 index 00000000000..bd793cc1c03 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/SourceContribution.java @@ -0,0 +1,79 @@ +/* + * Copyright 2012-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.antora; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.file.Directory; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.Zip; + +import org.springframework.boot.build.AntoraConventions; + +/** + * A contribution of source to Antora. + * + * @author Andy Wilkinson + */ +class SourceContribution extends Contribution { + + private static final String CONFIGURATION_NAME = "antoraSource"; + + SourceContribution(Project project, String name) { + super(project, name); + } + + void produce() { + Configuration antoraSource = getProject().getConfigurations().create(CONFIGURATION_NAME); + TaskProvider antoraSourceZip = getProject().getTasks().register("antoraSourceZip", Zip.class, (zip) -> { + zip.getDestinationDirectory().set(getProject().getLayout().getBuildDirectory().dir("antora-source")); + zip.from(AntoraConventions.ANTORA_SOURCE_DIR); + zip.setDescription( + "Creates a zip archive of the Antora source in %s.".formatted(AntoraConventions.ANTORA_SOURCE_DIR)); + }); + getProject().getArtifacts().add(antoraSource.getName(), antoraSourceZip); + } + + void consumeFrom(String path) { + Configuration configuration = createConfiguration(getName()); + DependencyHandler dependencies = getProject().getDependencies(); + dependencies.add(configuration.getName(), + getProject().provider(() -> projectDependency(path, CONFIGURATION_NAME))); + Provider outputDirectory = outputDirectory("source", getName()); + TaskContainer tasks = getProject().getTasks(); + TaskProvider syncSource = tasks.register(taskName("sync", "%s", configuration.getName()), + SyncAntoraSource.class, (task) -> configureSyncSource(task, path, configuration, outputDirectory)); + configureAntora(addInputFrom(syncSource, configuration.getName())); + configurePlaybookGeneration( + (generatePlaybook) -> generatePlaybook.getContentSource().addStartPath(outputDirectory)); + } + + private void configureSyncSource(SyncAntoraSource task, String path, Configuration configuration, + Provider outputDirectory) { + task.setDescription("Syncs the %s Antora source from %s.".formatted(getName(), path)); + task.setSource(configuration); + task.getOutputDirectory().set(outputDirectory); + } + + private Configuration createConfiguration(String name) { + return getProject().getConfigurations().create(configurationName(name, "AntoraSource")); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/SyncAntoraSource.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/SyncAntoraSource.java new file mode 100644 index 00000000000..6e955c8a38c --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/SyncAntoraSource.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-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.antora; + +import javax.inject.Inject; + +import org.gradle.api.DefaultTask; +import org.gradle.api.file.ArchiveOperations; +import org.gradle.api.file.CopySpec; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileSystemOperations; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.TaskAction; + +/** + * Task sync Antora source. + * + * @author Andy Wilkinson + */ +public abstract class SyncAntoraSource extends DefaultTask { + + private final FileSystemOperations fileSystemOperations; + + private final ArchiveOperations archiveOperations; + + private FileCollection source; + + @Inject + public SyncAntoraSource(FileSystemOperations fileSystemOperations, ArchiveOperations archiveOperations) { + this.fileSystemOperations = fileSystemOperations; + this.archiveOperations = archiveOperations; + } + + @OutputDirectory + public abstract DirectoryProperty getOutputDirectory(); + + @InputFiles + public FileCollection getSource() { + return this.source; + } + + public void setSource(FileCollection source) { + this.source = source; + } + + @TaskAction + void syncAntoraSource() { + this.fileSystemOperations.sync(this::syncAntoraSource); + } + + private void syncAntoraSource(CopySpec sync) { + sync.into(getOutputDirectory()); + this.source.getFiles().forEach((file) -> sync.from(this.archiveOperations.zipTree(file))); + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/antora/GenerateAntoraPlaybookTests.java b/buildSrc/src/test/java/org/springframework/boot/build/antora/GenerateAntoraPlaybookTests.java index c996fdcb998..dfa2676f20d 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/antora/GenerateAntoraPlaybookTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/antora/GenerateAntoraPlaybookTests.java @@ -19,13 +19,15 @@ package org.springframework.boot.build.antora; import java.io.File; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; +import java.util.List; import org.gradle.api.Project; import org.gradle.testfixtures.ProjectBuilder; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.springframework.boot.build.antora.Extensions.AntoraExtensionsConfiguration.ZipContentsCollector.AlwaysInclude; +import org.springframework.boot.build.antora.GenerateAntoraPlaybook.AntoraExtensions.ZipContentsCollector; import org.springframework.util.function.ThrowingConsumer; import static org.assertj.core.api.Assertions.assertThat; @@ -43,23 +45,26 @@ class GenerateAntoraPlaybookTests { @Test void writePlaybookGeneratesExpectedContent() throws Exception { writePlaybookYml((task) -> { - task.getXrefStubs().addAll("appendix:.*", "api:.*", "reference:.*"); - task.getAlwaysInclude().set(Map.of("name", "test", "classifier", "local-aggregate-content")); + task.getAntoraExtensions().getXref().getStubs().addAll("appendix:.*", "api:.*", "reference:.*"); + ZipContentsCollector zipContentsCollector = task.getAntoraExtensions().getZipContentsCollector(); + zipContentsCollector.getAlwaysInclude().set(List.of(new AlwaysInclude("test", "local-aggregate-content"))); + zipContentsCollector.getDependencies().add("test-dependency"); }); String actual = Files.readString(this.temp.toPath() .resolve("rootproject/project/build/generated/docs/antora-playbook/antora-playbook.yml")); String expected = Files .readString(Path.of("src/test/resources/org/springframework/boot/build/antora/expected-playbook.yml")); - System.out.println(actual); assertThat(actual.replace('\\', '/')).isEqualToNormalizingNewlines(expected.replace('\\', '/')); } @Test void writePlaybookWhenHasJavadocExcludeGeneratesExpectedContent() throws Exception { writePlaybookYml((task) -> { - task.getXrefStubs().addAll("appendix:.*", "api:.*", "reference:.*"); - task.getAlwaysInclude().set(Map.of("name", "test", "classifier", "local-aggregate-content")); - task.getExcludeJavadocExtension().set(true); + task.getAntoraExtensions().getXref().getStubs().addAll("appendix:.*", "api:.*", "reference:.*"); + ZipContentsCollector zipContentsCollector = task.getAntoraExtensions().getZipContentsCollector(); + zipContentsCollector.getAlwaysInclude().set(List.of(new AlwaysInclude("test", "local-aggregate-content"))); + zipContentsCollector.getDependencies().add("test-dependency"); + task.getAsciidocExtensions().getExcludeJavadocExtension().set(true); }); String actual = Files.readString(this.temp.toPath() .resolve("rootproject/project/build/generated/docs/antora-playbook/antora-playbook.yml")); diff --git a/buildSrc/src/test/resources/org/springframework/boot/build/antora/expected-playbook.yml b/buildSrc/src/test/resources/org/springframework/boot/build/antora/expected-playbook.yml index 340924854f5..e2871e032c2 100644 --- a/buildSrc/src/test/resources/org/springframework/boot/build/antora/expected-playbook.yml +++ b/buildSrc/src/test/resources/org/springframework/boot/build/antora/expected-playbook.yml @@ -13,7 +13,7 @@ antora: name: test locations: - project/build/generated/docs/antora-content/test-${version}-${name}-${classifier}.zip - - project/build/generated/docs/antora-dependencies-content/test-${version}-${name}-${classifier}.zip + - project/build/generated/docs/antora-dependencies-content/test-dependency/test-${version}-${name}-${classifier}.zip version_file: gradle.properties - require: '@springio/antora-extensions/root-component-extension' root_component_name: boot diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle index 2a0f9500f79..39823062551 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/build.gradle @@ -1,6 +1,6 @@ plugins { id "java-library" - id "org.antora" + id "org.springframework.boot.antora-contributor" id "org.springframework.boot.auto-configuration" id "org.springframework.boot.configuration-properties" id "org.springframework.boot.deployed" @@ -9,10 +9,6 @@ plugins { description = "Spring Boot Actuator AutoConfigure" -configurations { - antoraContent -} - dependencies { api(project(":spring-boot-project:spring-boot-actuator")) api(project(":spring-boot-project:spring-boot")) @@ -215,36 +211,18 @@ def documentationTest = tasks.register("documentationTest", Test) { } } -def antoraActuatorRestApiLocalAggregateContent = tasks.register("antoraActuatorRestApiLocalAggregateContent", Zip) { - destinationDirectory = layout.buildDirectory.dir('generated/docs/antora-content') - archiveClassifier = "actuator-rest-api-local-aggregate-content" - from(tasks.getByName("generateAntoraYml")) { - into "modules" +antoraContributions { + 'actuator-rest-api' { + aggregateContent { + from(documentationTest.map { layout.buildDirectory.dir("generated-snippets") }) { + into "modules/api/partials/rest/actuator" + } + } + localAggregateContent { + from(tasks.named("generateAntoraYml")) { + into "modules" + } + } + source() } } - -def antoraActuatorRestApiAggregateContent = tasks.register("antoraActuatorRestApiAggregateContent", Zip) { - dependsOn documentationTest - inputs.dir(layout.buildDirectory.dir("generated-snippets")) - .withPathSensitivity(PathSensitivity.RELATIVE) - .withPropertyName("generatedSnippets") - destinationDirectory = layout.buildDirectory.dir('generated/docs/antora-content') - archiveClassifier = "actuator-rest-api-aggregate-content" - from(layout.buildDirectory.dir("generated-snippets")) { - into "modules/api/partials/rest/actuator" - } -} - -tasks.named("generateAntoraPlaybook") { - alwaysInclude = [name: "actuator-rest-api", classifier: "local-aggregate-content"] - dependsOn antoraActuatorRestApiLocalAggregateContent -} - -tasks.named("antora") { - inputs.files(antoraActuatorRestApiAggregateContent) -} - -artifacts { - antoraContent antoraActuatorRestApiAggregateContent -} - diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 13aa4e263b6..7d5de4f2fc8 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -2,6 +2,8 @@ plugins { id "dev.adamko.dokkatoo-html" id "java" id "org.antora" + id "org.springframework.boot.antora-contributor" + id "org.springframework.boot.antora-dependencies" id "org.springframework.boot.deployed" id 'org.jetbrains.kotlin.jvm' } @@ -14,7 +16,6 @@ configurations { remoteSpringApplicationExample springApplicationExample testSlices - antoraContent all { resolutionStrategy.eachDependency { DependencyResolveDetails details -> if (details.requested.module.group == "org.apache.kafka" && details.requested.module.name == "kafka-server-common") { @@ -179,10 +180,6 @@ dependencies { springApplicationExample(platform(project(":spring-boot-project:spring-boot-dependencies"))) springApplicationExample(project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) - antoraContent(project(path: ":spring-boot-project:spring-boot-actuator-autoconfigure", configuration: "antoraContent")) - antoraContent(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin", configuration: "antoraContent")) - antoraContent(project(path: ":spring-boot-project:spring-boot-tools:spring-boot-maven-plugin", configuration: "antoraContent")) - testImplementation(project(":spring-boot-project:spring-boot-actuator-autoconfigure")) testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) testImplementation("org.assertj:assertj-core") @@ -308,86 +305,86 @@ def getRelativeExamplesPath(var outputs) { 'example$example-output/' + fileName } -def antoraRootAggregateContent = tasks.register("antoraRootAggregateContent", Zip) { - destinationDirectory = layout.buildDirectory.dir('generated/docs/antora-content') - archiveClassifier = "root-aggregate-content" - from("src/main") { - into "modules/ROOT/examples" +antoraDependencies { + 'actuator-rest-api' { + path = ":spring-boot-project:spring-boot-actuator-autoconfigure" + source() + aggregateContent() } - from(project.configurations.configurationProperties) { - eachFile { - it.path = rootProject - .projectDir - .toPath() - .relativize(it.file.toPath()) - .toString() - .replace('\\', '/') - .replaceAll('.*/([^/]+)/build.*', 'modules/ROOT/partials/$1/spring-configuration-metadata.json') + 'gradle-plugin' { + path = ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin" + source() + catalogContent() + } + 'maven-plugin' { + path = ":spring-boot-project:spring-boot-tools:spring-boot-maven-plugin" + source() + catalogContent() + aggregateContent() + } +} + +antoraContributions { + 'api' { + catalogContent { + from(aggregatedJavadoc) { + into "java" + } + from(tasks.named("dokkatooGeneratePublicationHtml")) { + into "kotlin" + } } } - from(runRemoteSpringApplicationExample) { - into "modules/ROOT/examples" - } - from(documentDevtoolsPropertyDefaults) { - into "modules/ROOT/partials/propertydefaults" - } - from(documentStarters) { - into "modules/ROOT/partials/starters" - } - from(documentTestSlices) { - into "modules/appendix/partials/slices" - } - from(runSpringApplicationExample) { - into "modules/ROOT/partials/application" - } - from(runLoggingFormatExample) { - into "modules/ROOT/partials/logging" - } - from(documentDependencyVersionCoordinates) { - into "modules/appendix/partials/dependency-versions" - } - from(documentDependencyVersionProperties) { - into "modules/appendix/partials/dependency-versions" - } - from(documentAutoConfigurationClasses) { - into "modules/appendix/partials/auto-configuration-classes" - } - from(documentConfigurationProperties) { - into "modules/appendix/partials/configuration-properties" - } - from(tasks.getByName("generateAntoraYml")) { - into "modules" - } -} - -def antoraApiCatalogContent = tasks.register("antoraApiCatalogContent", Zip) { - destinationDirectory = layout.buildDirectory.dir('generated/docs/antora-content') - archiveClassifier = "api-catalog-content" - from(aggregatedJavadoc) { - into "java" - } - from(tasks.named("dokkatooGeneratePublicationHtml")) { - into "kotlin" - } -} - -def copyAntoraContentDependencies = tasks.register("copyAntoraContentDependencies", Copy) { - into layout.buildDirectory.dir('generated/docs/antora-dependencies-content') - from(configurations.antoraContent) - rename("spring-boot-actuator-autoconfigure", "spring-boot-docs") - rename("spring-boot-maven-plugin", "spring-boot-docs") - rename("spring-boot-gradle-plugin", "spring-boot-docs") -} - -tasks.named("antora") { - inputs.files(antoraRootAggregateContent, antoraApiCatalogContent, copyAntoraContentDependencies) -} - -gradle.projectsEvaluated { - def mavenPublication = publishing.publications.getByName("maven"); - configurations.antoraContent.dependencies.forEach { dependency -> - dependency.dependencyProject.configurations.getByName(dependency.targetConfiguration) - .artifacts.forEach(mavenPublication::artifact) + 'root' { + aggregateContent { + from("src/main") { + into "modules/ROOT/examples" + } + from(project.configurations.configurationProperties) { + eachFile { + it.path = rootProject + .projectDir + .toPath() + .relativize(it.file.toPath()) + .toString() + .replace('\\', '/') + .replaceAll('.*/([^/]+)/build.*', 'modules/ROOT/partials/$1/spring-configuration-metadata.json') + } + } + from(runRemoteSpringApplicationExample) { + into "modules/ROOT/examples" + } + from(documentDevtoolsPropertyDefaults) { + into "modules/ROOT/partials/propertydefaults" + } + from(documentStarters) { + into "modules/ROOT/partials/starters" + } + from(documentTestSlices) { + into "modules/appendix/partials/slices" + } + from(runSpringApplicationExample) { + into "modules/ROOT/partials/application" + } + from(runLoggingFormatExample) { + into "modules/ROOT/partials/logging" + } + from(documentDependencyVersionCoordinates) { + into "modules/appendix/partials/dependency-versions" + } + from(documentDependencyVersionProperties) { + into "modules/appendix/partials/dependency-versions" + } + from(documentAutoConfigurationClasses) { + into "modules/appendix/partials/auto-configuration-classes" + } + from(documentConfigurationProperties) { + into "modules/appendix/partials/configuration-properties" + } + from(tasks.getByName("generateAntoraYml")) { + into "modules" + } + } } } @@ -396,12 +393,3 @@ dokkatoo { includes.from("src/docs/dokkatoo/dokka-overview.md") } } - -publishing { - publications { - getByName("maven") { - artifact antoraRootAggregateContent - artifact antoraApiCatalogContent - } - } -} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle index eb3cc5d8ec7..338335ea49c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/build.gradle @@ -5,7 +5,7 @@ import org.gradle.plugins.ide.eclipse.model.Library plugins { id "java-gradle-plugin" id "maven-publish" - id "org.antora" + id "org.springframework.boot.antora-contributor" id "org.springframework.boot.docker-test" id "org.springframework.boot.maven-repository" id "org.springframework.boot.optional-dependencies" @@ -14,7 +14,6 @@ plugins { description = "Spring Boot Gradle Plugins" configurations { - antoraContent "testCompileClasspath" { // Downgrade SLF4J is required for tests to run in Eclipse resolutionStrategy.force("org.slf4j:slf4j-api:1.7.36") @@ -166,35 +165,25 @@ javadoc { } } -def antoraGradlePluginLocalAggregateContent = tasks.register("antoraGradlePluginLocalAggregateContent", Zip) { - destinationDirectory = layout.buildDirectory.dir('generated/docs/antora-content') - archiveClassifier = "gradle-plugin-local-aggregate-content" - from(tasks.getByName("generateAntoraYml")) { - into "modules" - } -} - -def antoraGradlePluginCatalogContent = tasks.register("antoraGradlePluginCatalogContent", Zip) { - destinationDirectory = layout.buildDirectory.dir('generated/docs/antora-content') - archiveClassifier = "gradle-plugin-catalog-content" - from(javadoc) { - into "api/java" +antoraContributions { + 'gradle-plugin' { + catalogContent { + from(javadoc) { + into("api/java") + } + } + localAggregateContent { + from(tasks.named("generateAntoraYml")) { + into "modules" + } + } + source() } } tasks.named("generateAntoraPlaybook") { - xrefStubs = ["appendix:.*", "api:.*", "reference:.*"] - excludeJavadocExtension = true - alwaysInclude = [name: "gradle-plugin", classifier: "local-aggregate-content"] - dependsOn antoraGradlePluginLocalAggregateContent -} - -tasks.named("antora") { - inputs.files(antoraGradlePluginLocalAggregateContent, antoraGradlePluginCatalogContent) -} - -artifacts { - antoraContent antoraGradlePluginCatalogContent + antoraExtensions.xref.stubs = ["appendix:.*", "api:.*", "reference:.*"] + asciidocExtensions.excludeJavadocExtension = true } toolchain { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle index 0a968ca214a..83ba59974aa 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle +++ b/spring-boot-project/spring-boot-tools/spring-boot-maven-plugin/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.antora" + id "org.springframework.boot.antora-contributor" id "org.springframework.boot.maven-plugin" id "org.springframework.boot.optional-dependencies" id "org.springframework.boot.docker-test" @@ -9,7 +9,6 @@ description = "Spring Boot Maven Plugin" configurations { dependenciesBom - antoraContent } dependencies { @@ -148,45 +147,30 @@ tasks.named("documentPluginGoals") { ] } -def antoraMavenPluginLocalAggregateContent = tasks.register("antoraMavenPluginLocalAggregateContent", Zip) { - destinationDirectory = layout.buildDirectory.dir('generated/docs/antora-content') - archiveClassifier = "maven-plugin-local-aggregate-content" - from(tasks.getByName("generateAntoraYml")) { - into "modules" - } -} - -def antoraMavenPluginAggregateContent = tasks.register("antoraMavenPluginAggregateContent", Zip) { - destinationDirectory = layout.buildDirectory.dir('generated/docs/antora-content') - archiveClassifier = "maven-plugin-aggregate-content" - from(documentPluginGoals) { - into "modules/maven-plugin/partials/goals" - } -} - -def antoraMavenPluginCatalogContent = tasks.register("antoraMavenPluginCatalogContent", Zip) { - destinationDirectory = layout.buildDirectory.dir('generated/docs/antora-content') - archiveClassifier = "maven-plugin-catalog-content" - from(javadoc) { - into "api/java" +antoraContributions { + 'maven-plugin' { + aggregateContent { + from(documentPluginGoals) { + into "modules/maven-plugin/partials/goals" + } + } + catalogContent { + from(javadoc) { + into "api/java" + } + } + localAggregateContent { + from(tasks.named("generateAntoraYml")) { + into "modules" + } + } + source() } } tasks.named("generateAntoraPlaybook") { - xrefStubs = ["appendix:.*", "api:.*", "reference:.*", "how-to:.*"] - excludeJavadocExtension = true - alwaysInclude = [name: "maven-plugin", classifier: "local-aggregate-content"] - dependsOn antoraMavenPluginLocalAggregateContent -} - - -tasks.named("antora") { - inputs.files(antoraMavenPluginLocalAggregateContent, antoraMavenPluginAggregateContent, antoraMavenPluginCatalogContent) -} - -artifacts { - antoraContent antoraMavenPluginAggregateContent - antoraContent antoraMavenPluginCatalogContent + antoraExtensions.xref.stubs = ["appendix:.*", "api:.*", "reference:.*", "how-to:.*"] + asciidocExtensions.excludeJavadocExtension = true } tasks.named("dockerTest").configure {