diff --git a/.bomr/bomr.yaml b/.bomr/bomr.yaml
deleted file mode 100644
index a9ac945f30..0000000000
--- a/.bomr/bomr.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-bomr:
- bom: spring-boot-project/spring-boot-dependencies/pom.xml
- upgrade:
- github:
- organization: spring-projects
- repository: spring-boot
- issue-labels:
- - 'type: dependency-upgrade'
- policy: same-major-version
- prohibited:
- - project: derby
- versions:
- # 10.15 requires Java 9
- - '[10.15,)'
- verify:
- ignored-dependencies:
- # Avoid conflicting transitive requirements for
- # io.grpc:grpc-core:jar:[1.0.1,1.0.1] (Jetty),
- # io.grpc:grpc-core:jar:[1.14.0,1.14.0] (Micrometer's Azure Registry), and
- # io.grpc:grpc-core:jar:[1.15.0,1.15.0] (Micrometer's Stackdriver Registry)
- - 'io.micrometer:micrometer-registry-azure-monitor'
- - 'org.eclipse.jetty.gcloud:jetty-gcloud-session-manager'
- - 'org.eclipse.jetty:jetty-home'
- repositories:
- # Caffeine Simulator's dependencies
- - 'https://maven.imagej.net/content/repositories/public/'
- # Spring Data GemFire's GemFire dependencies
- - 'https://repo.spring.io/gemstone-release-pivotal-cache'
diff --git a/.gitignore b/.gitignore
index 4dbf2657a9..d37eb49a80 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,12 +22,16 @@ MANIFEST.MF
_site/
activemq-data
bin
+build
+!/**/src/**/bin
+!/**/src/**/build
build.log
dependency-reduced-pom.xml
dump.rdb
interpolated*.xml
lib/
manifest.yml
+out
overridedb.*
target
transaction-logs
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
deleted file mode 100644
index f432c96022..0000000000
--- a/.mvn/jvm.config
+++ /dev/null
@@ -1 +0,0 @@
--Xmx1536m
\ No newline at end of file
diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar
deleted file mode 100755
index e89f07c229..0000000000
Binary files a/.mvn/wrapper/maven-wrapper.jar and /dev/null differ
diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
deleted file mode 100755
index a84b7ef2d6..0000000000
--- a/.mvn/wrapper/maven-wrapper.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.5.4/apache-maven-3.5.4-bin.zip
-wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.3/maven-wrapper-0.5.3.jar
diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc
index 6c3c7c551b..04a4ef94b7 100755
--- a/CONTRIBUTING.adoc
+++ b/CONTRIBUTING.adoc
@@ -49,11 +49,13 @@ added after the original pull request but before a merge.
* We use the https://github.com/spring-io/spring-javaformat/[Spring JavaFormat] project
to apply code formatting conventions. If you use Eclipse and you follow the '`Importing
into eclipse`' instructions below you should get project specific formatting
- automatically. You can also install the https://github.com/spring-io/spring-javaformat/#intellij-idea[Spring JavaFormat IntelliJ Plugin]
- or format the code from the Maven build by running
- `./mvnw io.spring.javaformat:spring-javaformat-maven-plugin:apply`.
+ automatically. You can also install the
+ https://github.com/spring-io/spring-javaformat/#intellij-idea[Spring JavaFormat IntelliJ
+ Plugin] or format the code from the Gradle build by running
+ `./gradlew format`.
* The build includes checkstyle rules for many of our code conventions. Run
- `./mvnw validate` if you want to check you changes are compliant.
+ `./gradlew checkstyleMain checkstyleTest` if you want to check you changes are
+ compliant.
* Make sure all new `.java` files to have a simple Javadoc class comment with at least an
`@author` tag identifying you, and preferably at least a paragraph on what the class is
for.
@@ -75,65 +77,24 @@ added after the original pull request but before a merge.
If you don't have an IDE preference we would recommend that you use
https://spring.io/tools/sts[Spring Tools Suite] or
https://eclipse.org[Eclipse] when working with the code. We use the
-https://eclipse.org/m2e/[M2Eclipse] eclipse plugin for maven support. Other IDEs and tools
-should also work without issue.
+https://projects.eclipse.org/projects/tools.buildship[Buildship] Eclipse plugin for Gradle
+support. Other IDEs and tools should also work without issue.
=== Building from Source
-Spring Boot source can be built from the command line using
-https://maven.apache.org/run-maven/index.html[Apache Maven] on JDK 1.8 or above. We
-include '`Maven Wrapper`' scripts (`./mvnw` or `mvnw.bat`) that you can run rather than
-needing to install Maven locally.
+Spring Boot source can be built from the command line using https://gradle.org[Gradle] on
+JDK 1.8 or above. We include https://docs.gradle.org/current/userguide/gradle_wrapper.html[Gradle's
+wrapper scripts] (`./gradlew` or `gradlew.bat`) that you can run rather than needing to
+install Gradle locally.
-
-
-==== Default Build
-The project can be built from the root directory using the standard Maven command:
-
-[indent=0]
-----
- $ ./mvnw clean install
-----
-
-NOTE: You may need to increase the amount of memory available to Maven by setting
-a `MAVEN_OPTS` environment variable with the value `-Xmx512m`
-
-If you are rebuilding often, you might also want to skip the tests and the execution of
-checkstyle until you are ready to submit a pull request:
-
-[indent=0]
-----
- $ ./mvnw clean install -DskipTests -Pfast
-----
-
-
-
-==== Full Build
-You can run a full build using the following command:
+The project can be built from the root directory using the standard Gradle command:
[indent=0]
----
- $ ./mvnw -Pfull clean install
+ $ ./gradlew build
----
-NOTE: As for the standard build, you may need to increase the amount of memory available
-to Maven by setting a `MAVEN_OPTS` environment variable with the value `-Xmx512m`. We
-generate more artifacts when running the full build (such as Javadoc jars), so you may
-find the process a little slower than the standard build.
-
-[TIP]
-====
-If you want to run a build without the smoke tests and integration tests, building the
-`spring-boot-project` module is enough. You can cd there and run the same command, or you
-can run this from the top-level directory:
-
-[indent=0]
-----
- $ ./mvnw -f spring-boot-project -Pfull clean install
-----
-====
-
=== Importing into Eclipse
@@ -141,9 +102,6 @@ You can import the Spring Boot code into any Eclipse 2019-09-based distribution.
easiest way to setup a new environment is to use the Eclipse Installer with the provided
`spring-boot-project.setup` file (in the `/eclipse` folder).
-NOTE: Due to m2e issue https://bugs.eclipse.org/bugs/show_bug.cgi?id=548652[#548652] you need to be running m2e 1.14.0 or higher.
-An early milestone is available from https://download.eclipse.org/technology/m2e/milestones/1.14/.
-
==== Using the Eclipse Installer
@@ -169,9 +127,6 @@ Once complete you should find that a local workspace has been provisioned comple
all required Eclipse plugins. Projects will be grouped into working-sets to make the code
easier to navigate.
-If you want to work on the `spring-boot-gradle-plugin` you should remove the imported Maven
-project and reimport it as a Gradle project.
-
TIP: If you see import errors with `com.sun` packages make sure you have setup a valid
`JavaSE-1.8` environment. From preferences select "`Java`", "`Installed JREs`",
"`Execution Environments`" and make sure "`JavaSE-1.8`" points to a Java 1.8
@@ -179,13 +134,13 @@ install (we use AdoptOpenJDK on our CI).
-==== Manual Installation with M2Eclipse
+==== Manual Installation with Buildship
If you prefer to install Eclipse yourself you should use the
-https://eclipse.org/m2e/[M2Eclipse] eclipse plugin. If you don't already have m2eclipse
-installed it is available from the "`Eclipse marketplace`".
+https://projects.eclipse.org/projects/tools.buildship[Buildship] Eclipse plugin. If you
+don't already have Buildship installed it is available from the "`Eclipse marketplace`".
Spring Boot includes project specific source formatting settings, in order to have these
-work with m2eclipse, we provide an additional Eclipse plugin that you can install:
+work with Buildship, we provide an additional Eclipse plugin that you can install:
@@ -197,21 +152,13 @@ work with m2eclipse, we provide an additional Eclipse plugin that you can instal
NOTE: The plugin is optional. Projects can be imported without the plugins, your code
changes just won't be automatically formatted.
-With the requisite eclipse plugins installed you can select
-`import existing maven projects` from the `file` menu to import the code. You will
-need to import the root `spring-boot` pom and the `spring-boot-smoke-tests` pom separately.
+With the requisite Eclipse plugins installed you can select
+`Gradle -> Existing Gradle project` from the `File -> Import…` menu to import the code.
=== Importing into IntelliJ IDEA
-**Please, do this first!**
-Go to `Preferences | Build, Execution, Deployment | Build Tools | Maven | Importing`
-and set `VM options for importer` to `-Xmx2g` to allocate sufficient memory for IDEA's
-Maven import process to parse the Spring Boot project structure. _Not doing so could
-mean the import fails silently, leaving the project setup incomplete._
-
-For the actual import use "`File`" -> "`Open`" and select the root `pom.xml`, or the
-`spring-boot-project/pom.xml` if you only want the Spring Boot project sources.
+Use "`File`" -> "`Open`" and then select the root `build.gradle` file to import the code.
@@ -239,16 +186,8 @@ needs to be added.
=== Importing into Other IDEs
-Maven is well supported by most Java IDEs. Refer to your vendor documentation.
-
-
+Gradle is well supported by most Java IDEs. Refer to your vendor documentation.
-== Integration Tests
-The smoke tests run as part of the build when you `./mvnw install`.
-Due to the fact that they make use of the `spring-boot-maven-plugin`
-they cannot be called directly, and so instead are launched via the
-`maven-invoker-plugin`. If you encounter build failures running the integration tests,
-check the `build.log` file in the appropriate smoke test directory.
== Cloning the git repository on Windows
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000000..bc35a69d00
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,20 @@
+plugins {
+ id 'org.jetbrains.kotlin.jvm' apply false // https://youtrack.jetbrains.com/issue/KT-30276
+}
+
+description = 'Spring Boot Build'
+
+allprojects {
+ group 'org.springframework.boot'
+
+ repositories {
+ mavenCentral()
+ maven { url 'https://repo.spring.io/milestone' }
+ maven { url 'https://repo.spring.io/snapshot' }
+ }
+
+ configurations.all {
+ resolutionStrategy.cacheChangingModulesFor 60, 'minutes'
+ }
+
+}
diff --git a/buildSrc/README.adoc b/buildSrc/README.adoc
new file mode 100644
index 0000000000..44c7bc7f43
--- /dev/null
+++ b/buildSrc/README.adoc
@@ -0,0 +1,26 @@
+= BOM Plugin
+
+Allows one to publish a BOM from Gradle.
+Properties, dependencies, and other BOMs are applied to the `bom {}` extension.
+
+This plugin applies the `java-platform` and `maven-publish` plugins to the Project it's applied to.
+
+== Usage
+
+[source,groovy,indent=0]
+----
+plugins {
+ id 'org.springframework.boot.bom'
+}
+
+bom {
+ property 'logback.version', '1.2.3'
+ property 'junit-jupiter.version', '5.3.2'
+
+ dependency 'ch.qos.logback', 'logback-classic', '${logback.version}'
+ dependency 'ch.qos.logback', 'logback-core', '${logback.version}'
+
+ bomImport 'org.junit', 'junit-bom', '${junit-jupiter.version}'
+}
+----
+
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
new file mode 100644
index 0000000000..b0587d4d33
--- /dev/null
+++ b/buildSrc/build.gradle
@@ -0,0 +1,84 @@
+plugins {
+ id 'java-gradle-plugin'
+ id 'io.spring.javaformat' version '0.0.18-SNAPSHOT'
+ id 'checkstyle'
+}
+
+repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ maven { url 'https://repo.spring.io/release' }
+}
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+dependencies {
+ checkstyle 'io.spring.javaformat:spring-javaformat-checkstyle:0.0.15'
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.10.0'
+ implementation 'commons-codec:commons-codec:1.13'
+ implementation 'org.apache.maven:maven-embedder:3.6.2'
+ implementation 'org.asciidoctor:asciidoctor-gradle-jvm:2.4.0'
+ implementation 'org.springframework:spring-core:5.2.2.RELEASE'
+ implementation 'org.springframework:spring-web:5.2.2.RELEASE'
+ implementation 'com.google.code.gson:gson:2.8.5'
+ implementation 'io.spring.javaformat:spring-javaformat-gradle-plugin:0.0.15'
+ testImplementation 'org.assertj:assertj-core:3.11.1'
+ testImplementation 'org.apache.logging.log4j:log4j-core:2.12.1'
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.5.2'
+}
+
+checkstyle {
+ def archive = configurations.checkstyle.filter { it.name.startsWith('spring-javaformat-checkstyle')}
+ config = resources.text.fromArchiveEntry(archive, 'io/spring/javaformat/checkstyle/checkstyle.xml')
+ toolVersion = 8.11
+}
+
+gradlePlugin {
+ plugins {
+ autoConfigurationPlugin {
+ id = "org.springframework.boot.auto-configuration"
+ implementationClass = "org.springframework.boot.build.autoconfigure.AutoConfigurationPlugin"
+ }
+ bomPlugin {
+ id = "org.springframework.boot.bom"
+ implementationClass = "org.springframework.boot.build.bom.BomPlugin"
+ }
+ configurationPropertiesPlugin {
+ id = "org.springframework.boot.configuration-properties"
+ implementationClass = "org.springframework.boot.build.context.properties.ConfigurationPropertiesPlugin"
+ }
+ conventionsPlugin {
+ id = "org.springframework.boot.conventions"
+ implementationClass = "org.springframework.boot.build.ConventionsPlugin"
+ }
+ deployedPlugin {
+ id = "org.springframework.boot.deployed"
+ implementationClass = "org.springframework.boot.build.DeployedPlugin"
+ }
+ integrationTestPlugin {
+ id = "org.springframework.boot.integration-test"
+ implementationClass = "org.springframework.boot.build.test.IntegrationTestPlugin"
+ }
+ mavenPluginPlugin {
+ id = "org.springframework.boot.maven-plugin"
+ implementationClass = "org.springframework.boot.build.mavenplugin.MavenPluginPlugin"
+ }
+ mavenRepositoryPlugin {
+ id = "org.springframework.boot.maven-repository"
+ implementationClass = "org.springframework.boot.build.MavenRepositoryPlugin"
+ }
+ optionalDependenciesPlugin {
+ id = "org.springframework.boot.optional-dependencies"
+ implementationClass = "org.springframework.boot.build.optional.OptionalDependenciesPlugin"
+ }
+ starterPlugin {
+ id = "org.springframework.boot.starter"
+ implementationClass = "org.springframework.boot.build.starters.StarterPlugin"
+ }
+ }
+}
+
+test {
+ useJUnitPlatform()
+}
diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle
new file mode 100644
index 0000000000..ce6d3a43da
--- /dev/null
+++ b/buildSrc/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ maven { url 'https://repo.spring.io/snapshot' }
+ }
+ resolutionStrategy {
+ eachPlugin {
+ if (requested.id.id == 'io.spring.javaformat') {
+ useModule "io.spring.javaformat:spring-javaformat-gradle-plugin:${requested.version}"
+ }
+ }
+ }
+}
+
+apply from: new File(settingsDir, '../gradle/build-cache-settings.gradle')
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/AsciidoctorConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/AsciidoctorConventions.java
new file mode 100644
index 0000000000..132940f685
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/AsciidoctorConventions.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2019 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;
+
+import java.io.File;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask;
+import org.asciidoctor.gradle.jvm.AsciidoctorJExtension;
+import org.asciidoctor.gradle.jvm.AsciidoctorJPlugin;
+import org.asciidoctor.gradle.jvm.AsciidoctorTask;
+import org.gradle.api.Action;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.Project;
+import org.gradle.api.Task;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.Sync;
+import org.gradle.api.tasks.TaskAction;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * Conventions that are applied in the presence of the {@link AsciidoctorJPlugin}. When
+ * the plugin is applied:
+ *
+ *
+ * All warnings are made fatal.
+ * A task is created to resolve and unzip our documentation resources (CSS and
+ * Javascript).
+ * For each {@link AsciidoctorTask} (HTML only):
+ *
+ * A task is created to sync the documentation resources to its output directory.
+ * {@code doctype} {@link AsciidoctorTask#options(Map) option} is configured.
+ * {@link AsciidoctorTask#attributes(Map) Attributes} are configured for syntax
+ * highlighting, CSS styling, docinfo, etc.
+ *
+ * For each {@link AbstractAsciidoctorTask} (HTML and PDF):
+ *
+ * {@link AsciidoctorTask#attributes(Map) Attributes} are configured to enable
+ * warnings for references to missing attributes, the GitHub tag, the Artifactory repo for
+ * the current version, etc.
+ * {@link AbstractAsciidoctorTask#baseDirFollowsSourceDir() baseDirFollowsSourceDir()}
+ * is enabled.
+ *
+ *
+ *
+ * @author Andy Wilkinson
+ */
+class AsciidoctorConventions {
+
+ void apply(Project project) {
+ project.getPlugins().withType(AsciidoctorJPlugin.class, (asciidoctorPlugin) -> {
+ configureDocResourcesRepository(project);
+ makeAllWarningsFatal(project);
+ UnzipDocumentationResources unzipResources = createUnzipDocumentationResourcesTask(project);
+ project.getTasks().withType(AbstractAsciidoctorTask.class, (asciidoctorTask) -> {
+ configureCommonAttributes(project, asciidoctorTask);
+ configureOptions(asciidoctorTask);
+ asciidoctorTask.baseDirFollowsSourceDir();
+ Sync syncSource = createSyncDocumentationSourceTask(project, asciidoctorTask);
+ if (asciidoctorTask instanceof AsciidoctorTask) {
+ configureHtmlOnlyAttributes(project, asciidoctorTask);
+ syncSource.from(unzipResources, (resources) -> resources.into("asciidoc"));
+ asciidoctorTask.doFirst(new Action() {
+
+ @Override
+ public void execute(Task task) {
+ project.copy((spec) -> {
+ spec.from(asciidoctorTask.getSourceDir());
+ spec.into(asciidoctorTask.getOutputDir());
+ spec.include("css/**", "js/**");
+ });
+ }
+
+ });
+ }
+ });
+ });
+ }
+
+ private void configureDocResourcesRepository(Project project) {
+ project.getRepositories().maven((mavenRepo) -> {
+ mavenRepo.setUrl(URI.create("https://repo.spring.io/release"));
+ mavenRepo.mavenContent((mavenContent) -> mavenContent.includeGroup("io.spring.docresources"));
+ });
+ }
+
+ private void makeAllWarningsFatal(Project project) {
+ project.getExtensions().getByType(AsciidoctorJExtension.class).fatalWarnings(".*");
+ }
+
+ private UnzipDocumentationResources createUnzipDocumentationResourcesTask(Project project) {
+ Configuration documentationResources = project.getConfigurations().maybeCreate("documentationResources");
+ documentationResources.getDependencies()
+ .add(project.getDependencies().create("io.spring.docresources:spring-doc-resources:0.1.3.RELEASE"));
+ UnzipDocumentationResources unzipResources = project.getTasks().create("unzipDocumentationResources",
+ UnzipDocumentationResources.class);
+ unzipResources.setResources(documentationResources);
+ unzipResources.setOutputDir(new File(project.getBuildDir(), "docs/resources"));
+ return unzipResources;
+ }
+
+ private Sync createSyncDocumentationSourceTask(Project project, AbstractAsciidoctorTask asciidoctorTask) {
+ Sync syncDocumentationSource = project.getTasks()
+ .create("syncDocumentationSourceFor" + StringUtils.capitalize(asciidoctorTask.getName()), Sync.class);
+ File syncedSource = new File(project.getBuildDir(), "docs/src/" + asciidoctorTask.getName());
+ syncDocumentationSource.setDestinationDir(syncedSource);
+ syncDocumentationSource.from("src/docs/");
+ asciidoctorTask.dependsOn(syncDocumentationSource);
+ asciidoctorTask.setSourceDir(project.relativePath(new File(syncedSource, "asciidoc/")));
+ return syncDocumentationSource;
+ }
+
+ private void configureOptions(AbstractAsciidoctorTask asciidoctorTask) {
+ asciidoctorTask.options(Collections.singletonMap("doctype", "book"));
+ }
+
+ private void configureHtmlOnlyAttributes(Project project, AbstractAsciidoctorTask asciidoctorTask) {
+ Map attributes = new HashMap<>();
+ attributes.put("highlightjsdir", "js/highlight");
+ attributes.put("highlightjs-theme", "github");
+ attributes.put("linkcss", true);
+ attributes.put("icons", "font");
+ attributes.put("stylesheet", "css/spring.css");
+ asciidoctorTask.attributes(attributes);
+ }
+
+ private void configureCommonAttributes(Project project, AbstractAsciidoctorTask asciidoctorTask) {
+ Map attributes = new HashMap<>();
+ attributes.put("attribute-missing", "warn");
+ attributes.put("github-tag", determineGitHubTag(project));
+ attributes.put("spring-boot-artifactory-repo", determineArtifactoryRepo(project));
+ attributes.put("version", "{gradle-project-version}");
+ asciidoctorTask.attributes(attributes);
+ }
+
+ private String determineArtifactoryRepo(Project project) {
+ String version = project.getVersion().toString();
+ String type = version.substring(version.lastIndexOf('.'));
+ if (type.equals("RELEASE")) {
+ return "release";
+ }
+ if (type.startsWith("M") || type.startsWith("RC")) {
+ return "milestone";
+ }
+ return "snapshot";
+ }
+
+ private String determineGitHubTag(Project project) {
+ String version = "v" + project.getVersion();
+ return (version.endsWith("-SNAPSHOT")) ? "master" : version;
+ }
+
+ /**
+ * {@link Task} for unzipping the documentation resources.
+ */
+ public static class UnzipDocumentationResources extends DefaultTask {
+
+ private FileCollection resources;
+
+ private File outputDir;
+
+ @InputFiles
+ public FileCollection getResources() {
+ return this.resources;
+ }
+
+ public void setResources(FileCollection resources) {
+ this.resources = resources;
+ }
+
+ @OutputDirectory
+ public File getOutputDir() {
+ return this.outputDir;
+ }
+
+ public void setOutputDir(File outputDir) {
+ this.outputDir = outputDir;
+ }
+
+ @TaskAction
+ void syncDocumentationResources() {
+ getProject().sync((copySpec) -> {
+ copySpec.into(this.outputDir);
+ for (File resource : this.resources) {
+ copySpec.from(getProject().zipTree(resource));
+ }
+ });
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java
new file mode 100644
index 0000000000..30aa02b726
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2012-2020 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;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import io.spring.javaformat.gradle.SpringJavaFormatPlugin;
+import org.apache.maven.artifact.repository.MavenArtifactRepository;
+import org.asciidoctor.gradle.jvm.AsciidoctorJPlugin;
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.DependencySet;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.plugins.JavaPluginExtension;
+import org.gradle.api.plugins.quality.CheckstyleExtension;
+import org.gradle.api.plugins.quality.CheckstylePlugin;
+import org.gradle.api.publish.PublishingExtension;
+import org.gradle.api.publish.maven.MavenPom;
+import org.gradle.api.publish.maven.MavenPomDeveloperSpec;
+import org.gradle.api.publish.maven.MavenPomIssueManagement;
+import org.gradle.api.publish.maven.MavenPomLicenseSpec;
+import org.gradle.api.publish.maven.MavenPomOrganization;
+import org.gradle.api.publish.maven.MavenPomScm;
+import org.gradle.api.publish.maven.MavenPublication;
+import org.gradle.api.publish.maven.plugins.MavenPublishPlugin;
+import org.gradle.api.tasks.bundling.Jar;
+import org.gradle.api.tasks.compile.JavaCompile;
+import org.gradle.api.tasks.javadoc.Javadoc;
+import org.gradle.api.tasks.testing.Test;
+
+/**
+ * Plugin to apply conventions to projects that are part of Spring Boot's build.
+ * Conventions are applied in response to various plugins being applied.
+ *
+ *
+ *
+ * When the {@link JavaPlugin Java plugin} is applied:
+ *
+ *
+ * {@code sourceCompatibility} is set to {@code 1.8}
+ * Spring Java Format and Checkstyle plugins are applied
+ * {@link Test} tasks are configured to use JUnit Platform and use a max heap of 1024M
+ * {@link JavaCompile} tasks are configured to use UTF-8 encoding
+ * {@link Javadoc} tasks are configured to use UTF-8 encoding
+ * {@link Jar} tasks are configured to have the following manifest entries:
+ *
+ * {@code Automatic-Module-Name}
+ * {@code Build-Jdk-Spec}
+ * {@code Built-By}
+ * {@code Implementation-Title}
+ * {@code Implementation-Version}
+ *
+ *
+ *
+ *
+ *
+ * When the {@link MavenPublishPlugin Maven Publish plugin} is applied:
+ *
+ *
+ * If the {@code deploymentRepository} property has been set, a
+ * {@link MavenArtifactRepository Maven artifact repository} is configured to publish to
+ * it.
+ * The poms of all {@link MavenPublication Maven publications} are customized to meet
+ * Maven Central's requirements.
+ * If the {@link JavaPlugin Java plugin} has also been applied, creation of Javadoc
+ * and source jars is enabled.
+ *
+ *
+ *
+ *
+ * When the {@link AsciidoctorJPlugin} is applied, the conventions in
+ * {@link AsciidoctorConventions} are applied.
+ *
+ * @author Andy Wilkinson
+ */
+public class ConventionsPlugin implements Plugin {
+
+ @Override
+ public void apply(Project project) {
+ applyJavaConventions(project);
+ applyAsciidoctorConventions(project);
+ applyMavenPublishingConventions(project);
+ }
+
+ private void applyJavaConventions(Project project) {
+ project.getPlugins().withType(JavaPlugin.class, (java) -> {
+ configureSpringJavaFormat(project);
+ project.setProperty("sourceCompatibility", "1.8");
+ project.getTasks().withType(JavaCompile.class, (compile) -> compile.getOptions().setEncoding("UTF-8"));
+ project.getTasks().withType(Javadoc.class,
+ (javadoc) -> javadoc.getOptions().source("1.8").encoding("UTF-8"));
+ project.getTasks().withType(Test.class, (test) -> {
+ test.useJUnitPlatform();
+ test.setMaxHeapSize("1024M");
+ });
+ project.getTasks().withType(Jar.class, (jar) -> {
+ project.afterEvaluate((evaluated) -> {
+ jar.manifest((manifest) -> {
+ Map attributes = new TreeMap<>();
+ attributes.put("Automatic-Module-Name", project.getName().replace("-", "."));
+ attributes.put("Build-Jdk-Spec", project.property("sourceCompatibility"));
+ attributes.put("Built-By", "Spring");
+ attributes.put("Implementation-Title", project.getDescription());
+ attributes.put("Implementation-Version", project.getVersion());
+ manifest.attributes(attributes);
+ });
+ });
+ });
+ });
+ }
+
+ private void configureSpringJavaFormat(Project project) {
+ project.getPlugins().apply(SpringJavaFormatPlugin.class);
+ project.getPlugins().apply(CheckstylePlugin.class);
+ CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
+ checkstyle.setToolVersion("8.22");
+ checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
+ String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
+ DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
+ checkstyleDependencies
+ .add(project.getDependencies().create("io.spring.javaformat:spring-javaformat-checkstyle:" + version));
+ checkstyleDependencies
+ .add(project.getDependencies().create("io.spring.nohttp:nohttp-checkstyle:0.0.3.RELEASE"));
+ }
+
+ private void applyAsciidoctorConventions(Project project) {
+ new AsciidoctorConventions().apply(project);
+ }
+
+ private void applyMavenPublishingConventions(Project project) {
+ project.getPlugins().withType(MavenPublishPlugin.class).all((mavenPublish) -> {
+ PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class);
+ if (project.hasProperty("deploymentRepository")) {
+ publishing.getRepositories().maven((mavenRepository) -> {
+ mavenRepository.setUrl(project.property("deploymentRepository"));
+ mavenRepository.setName("deployment");
+ });
+ }
+ publishing.getPublications().withType(MavenPublication.class)
+ .all((mavenPublication) -> customizePom(mavenPublication.getPom(), project));
+ project.getPlugins().withType(JavaPlugin.class).all((javaPlugin) -> {
+ JavaPluginExtension extension = project.getExtensions().getByType(JavaPluginExtension.class);
+ extension.withJavadocJar();
+ extension.withSourcesJar();
+ });
+ });
+ }
+
+ private void customizePom(MavenPom pom, Project project) {
+ pom.getUrl().set("https://projects.spring.io/spring-boot/#");
+ pom.getDescription().set(project.provider(project::getDescription));
+ pom.organization(this::customizeOrganization);
+ pom.licenses(this::customizeLicences);
+ pom.developers(this::customizeDevelopers);
+ pom.scm(this::customizeScm);
+ pom.issueManagement(this::customizeIssueManagement);
+ }
+
+ private void customizeOrganization(MavenPomOrganization organization) {
+ organization.getName().set("Pivotal Software, Inc.");
+ organization.getUrl().set("https://spring.io");
+ }
+
+ private void customizeLicences(MavenPomLicenseSpec licences) {
+ licences.license((licence) -> {
+ licence.getName().set("Apache License, Version 2.0");
+ licence.getUrl().set("http://www.apache.org/licenses/LICENSE-2.0");
+ });
+ }
+
+ private void customizeDevelopers(MavenPomDeveloperSpec developers) {
+ developers.developer((developer) -> {
+ developer.getName().set("Pivotal");
+ developer.getEmail().set("info@pivotal.io");
+ developer.getOrganization().set("Pivotal Software, Inc.");
+ developer.getOrganizationUrl().set("https://www.spring.io");
+ });
+ }
+
+ private void customizeScm(MavenPomScm scm) {
+ scm.getConnection().set("scm:git:git://github.com/spring-projects/spring-boot.git");
+ scm.getDeveloperConnection().set("scm:git:ssh://git@github.com/spring-projects/spring-boot.git");
+ scm.getUrl().set("https://github.com/spring-projects/spring-boot");
+
+ }
+
+ private void customizeIssueManagement(MavenPomIssueManagement issueManagement) {
+ issueManagement.getSystem().set("GitHub");
+ issueManagement.getUrl().set("https://github.com/spring-projects/spring-boot/issues");
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/DeployedPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/DeployedPlugin.java
new file mode 100644
index 0000000000..30dc9d3db1
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/DeployedPlugin.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019 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;
+
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.plugins.JavaPlatformPlugin;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.publish.PublishingExtension;
+import org.gradle.api.publish.maven.MavenPublication;
+import org.gradle.api.publish.maven.plugins.MavenPublishPlugin;
+
+/**
+ * A plugin applied to a project that should be deployed.
+ *
+ * @author Andy Wilkinson
+ */
+public class DeployedPlugin implements Plugin {
+
+ /**
+ * Name of the task that generates the deployed pom file.
+ */
+ public static final String GENERATE_POM_TASK_NAME = "generatePomFileForMavenPublication";
+
+ @Override
+ public void apply(Project project) {
+ project.getPlugins().apply(MavenPublishPlugin.class);
+ project.getPlugins().apply(MavenRepositoryPlugin.class);
+ PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class);
+ MavenPublication mavenPublication = publishing.getPublications().create("maven", MavenPublication.class);
+ project.getPlugins().withType(JavaPlugin.class)
+ .all((javaPlugin) -> project.getComponents().matching((component) -> component.getName().equals("java"))
+ .all((javaComponent) -> mavenPublication.from(javaComponent)));
+ project.getPlugins().withType(JavaPlatformPlugin.class)
+ .all((javaPlugin) -> project.getComponents()
+ .matching((component) -> component.getName().equals("javaPlatform"))
+ .all((javaComponent) -> mavenPublication.from(javaComponent)));
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/MavenRepositoryPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/MavenRepositoryPlugin.java
new file mode 100644
index 0000000000..d7fcbfdcbc
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/MavenRepositoryPlugin.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2012-2020 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;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.gradle.api.Action;
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.Task;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.DependencySet;
+import org.gradle.api.artifacts.ProjectDependency;
+import org.gradle.api.plugins.JavaLibraryPlugin;
+import org.gradle.api.plugins.JavaPlatformPlugin;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.publish.PublishingExtension;
+import org.gradle.api.publish.maven.plugins.MavenPublishPlugin;
+
+/**
+ * A plugin to make a project's {@code deployment} publication available as a Maven
+ * repository. The repository can be consumed by depending upon the project using the
+ * {@code mavenRepository} configuration.
+ *
+ * @author Andy Wilkinson
+ */
+public class MavenRepositoryPlugin implements Plugin {
+
+ /**
+ * Name of the {@code mavenRepository} configuration.
+ */
+ public static final String MAVEN_REPOSITORY_CONFIGURATION_NAME = "mavenRepository";
+
+ /**
+ * Name of the task that publishes to the project repository.
+ */
+ public static final String PUBLISH_TO_PROJECT_REPOSITORY_TASK_NAME = "publishMavenPublicationToProjectRepository";
+
+ @Override
+ public void apply(Project project) {
+ project.getPlugins().apply(MavenPublishPlugin.class);
+ PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class);
+ File repositoryLocation = new File(project.getBuildDir(), "maven-repository");
+ publishing.getRepositories().maven((mavenRepository) -> {
+ mavenRepository.setName("project");
+ mavenRepository.setUrl(repositoryLocation.toURI());
+ });
+ project.getTasks().matching((task) -> task.getName().equals(PUBLISH_TO_PROJECT_REPOSITORY_TASK_NAME))
+ .all((task) -> setUpProjectRepository(project, task, repositoryLocation));
+ project.getTasks().matching((task) -> task.getName().equals("publishPluginMavenPublicationToProjectRepository"))
+ .all((task) -> setUpProjectRepository(project, task, repositoryLocation));
+ }
+
+ private void setUpProjectRepository(Project project, Task publishTask, File repositoryLocation) {
+ publishTask.doFirst(new CleanAction(repositoryLocation));
+ Configuration projectRepository = project.getConfigurations().create(MAVEN_REPOSITORY_CONFIGURATION_NAME);
+ project.getArtifacts().add(projectRepository.getName(), repositoryLocation,
+ (artifact) -> artifact.builtBy(publishTask));
+ DependencySet target = projectRepository.getDependencies();
+ project.getPlugins().withType(JavaPlugin.class).all((javaPlugin) -> addMavenRepositoryDependencies(project,
+ JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME, target));
+ project.getPlugins().withType(JavaLibraryPlugin.class)
+ .all((javaLibraryPlugin) -> addMavenRepositoryDependencies(project, JavaPlugin.API_CONFIGURATION_NAME,
+ target));
+ project.getPlugins().withType(JavaPlatformPlugin.class)
+ .all((javaPlugin) -> addMavenRepositoryDependencies(project, JavaPlatformPlugin.API_CONFIGURATION_NAME,
+ target));
+ }
+
+ private void addMavenRepositoryDependencies(Project project, String sourceConfigurationName, DependencySet target) {
+ project.getConfigurations().getByName(sourceConfigurationName).getDependencies()
+ .withType(ProjectDependency.class).all((dependency) -> {
+ Map dependencyDescriptor = new HashMap<>();
+ dependencyDescriptor.put("path", dependency.getDependencyProject().getPath());
+ dependencyDescriptor.put("configuration", MAVEN_REPOSITORY_CONFIGURATION_NAME);
+ target.add(project.getDependencies().project(dependencyDescriptor));
+ });
+ }
+
+ private static final class CleanAction implements Action {
+
+ private final File location;
+
+ private CleanAction(File location) {
+ this.location = location;
+ }
+
+ @Override
+ public void execute(Task task) {
+ task.getProject().delete(this.location);
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationMetadata.java b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationMetadata.java
new file mode 100644
index 0000000000..c4664062b4
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationMetadata.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2012-2020 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.autoconfigure;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Properties;
+import java.util.concurrent.Callable;
+
+import org.gradle.api.Task;
+import org.gradle.api.internal.AbstractTask;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.SourceSet;
+import org.gradle.api.tasks.TaskAction;
+
+/**
+ * A {@link Task} for generating metadata describing a project's auto-configuration
+ * classes.
+ *
+ * @author Andy Wilkinson
+ */
+public class AutoConfigurationMetadata extends AbstractTask {
+
+ private SourceSet sourceSet;
+
+ private File outputFile;
+
+ public AutoConfigurationMetadata() {
+ getInputs().file((Callable) () -> new File(this.sourceSet.getOutput().getResourcesDir(),
+ "META-INF/spring.factories"));
+ dependsOn((Callable) () -> this.sourceSet.getProcessResourcesTaskName());
+ getProject().getConfigurations()
+ .maybeCreate(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME);
+ }
+
+ public void setSourceSet(SourceSet sourceSet) {
+ this.sourceSet = sourceSet;
+ }
+
+ @OutputFile
+ public File getOutputFile() {
+ return this.outputFile;
+ }
+
+ public void setOutputFile(File outputFile) {
+ this.outputFile = outputFile;
+ }
+
+ @TaskAction
+ void documentAutoConfiguration() throws IOException {
+ Properties autoConfiguration = readAutoConfiguration();
+ getOutputFile().getParentFile().mkdirs();
+ try (FileWriter writer = new FileWriter(getOutputFile())) {
+ autoConfiguration.store(writer, null);
+ }
+ }
+
+ private Properties readAutoConfiguration() throws IOException {
+ Properties autoConfiguration = new Properties();
+ Properties springFactories = readSpringFactories(
+ new File(this.sourceSet.getOutput().getResourcesDir(), "META-INF/spring.factories"));
+ autoConfiguration.setProperty("autoConfigurationClassNames",
+ springFactories.getProperty("org.springframework.boot.autoconfigure.EnableAutoConfiguration"));
+ autoConfiguration.setProperty("module", getProject().getName());
+ return autoConfiguration;
+ }
+
+ private Properties readSpringFactories(File file) throws IOException {
+ Properties springFactories = new Properties();
+ try (Reader in = new FileReader(file)) {
+ springFactories.load(in);
+ }
+ return springFactories;
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java
new file mode 100644
index 0000000000..7c2c77b4d2
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/AutoConfigurationPlugin.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2019 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.autoconfigure;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.concurrent.Callable;
+
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.plugins.JavaPluginConvention;
+import org.gradle.api.tasks.SourceSet;
+
+import org.springframework.boot.build.DeployedPlugin;
+import org.springframework.boot.build.context.properties.ConfigurationPropertiesPlugin;
+
+/**
+ * {@link Plugin} for projects that define auto-configuration. When applied, the plugin
+ * applies the {@link DeployedPlugin}. Additionally, it reacts to the presence of the
+ * {@link JavaPlugin} by:
+ *
+ *
+ * Applying the {@link ConfigurationPropertiesPlugin}.
+ * Adding a dependency on the auto-configuration annotation processor.
+ * Defining a task that produces metadata describing the auto-configuration. The
+ * metadata is made available as an artifact in the
+ *
+ *
+ * @author Andy Wilkinson
+ */
+public class AutoConfigurationPlugin implements Plugin {
+
+ /**
+ * Name of the {@link Configuration} that holds the auto-configuration metadata
+ * artifact.
+ */
+ public static final String AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME = "autoConfigurationMetadata";
+
+ @Override
+ public void apply(Project project) {
+ project.getPlugins().apply(DeployedPlugin.class);
+ project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
+ project.getPlugins().apply(ConfigurationPropertiesPlugin.class);
+ Configuration annotationProcessors = project.getConfigurations()
+ .getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME);
+ annotationProcessors.getDependencies()
+ .add(project.getDependencies().project(Collections.singletonMap("path",
+ ":spring-boot-project:spring-boot-tools:spring-boot-autoconfigure-processor")));
+ annotationProcessors.getDependencies()
+ .add(project.getDependencies().project(Collections.singletonMap("path",
+ ":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor")));
+ project.getTasks().create("autoConfigurationMetadata", AutoConfigurationMetadata.class, (task) -> {
+ task.setSourceSet(project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets()
+ .getByName(SourceSet.MAIN_SOURCE_SET_NAME));
+ task.setOutputFile(new File(project.getBuildDir(), "auto-configuration-metadata.properties"));
+ project.getArtifacts().add(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME,
+ project.provider((Callable) task::getOutputFile), (artifact) -> artifact.builtBy(task));
+ });
+ });
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/DocumentAutoConfigurationClasses.java b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/DocumentAutoConfigurationClasses.java
new file mode 100644
index 0000000000..6302342dd7
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/autoconfigure/DocumentAutoConfigurationClasses.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2012-2020 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.autoconfigure;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.util.Properties;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import org.gradle.api.Task;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.internal.AbstractTask;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.TaskAction;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link Task} used to document auto-configuration classes.
+ *
+ * @author Andy Wilkinson
+ */
+public class DocumentAutoConfigurationClasses extends AbstractTask {
+
+ private FileCollection autoConfiguration;
+
+ private File outputDir;
+
+ @InputFiles
+ public FileCollection getAutoConfiguration() {
+ return this.autoConfiguration;
+ }
+
+ public void setAutoConfiguration(FileCollection autoConfiguration) {
+ this.autoConfiguration = autoConfiguration;
+ }
+
+ @OutputDirectory
+ public File getOutputDir() {
+ return this.outputDir;
+ }
+
+ public void setOutputDir(File outputDir) {
+ this.outputDir = outputDir;
+ }
+
+ @TaskAction
+ void documentAutoConfigurationClasses() throws IOException {
+ for (File metadataFile : this.autoConfiguration) {
+ Properties metadata = new Properties();
+ try (Reader reader = new FileReader(metadataFile)) {
+ metadata.load(reader);
+ }
+ AutoConfiguration autoConfiguration = new AutoConfiguration(metadata.getProperty("module"), new TreeSet<>(
+ StringUtils.commaDelimitedListToSet(metadata.getProperty("autoConfigurationClassNames"))));
+ writeTable(autoConfiguration);
+ }
+ }
+
+ private void writeTable(AutoConfiguration autoConfigurationClasses) throws IOException {
+ this.outputDir.mkdirs();
+ try (PrintWriter writer = new PrintWriter(
+ new FileWriter(new File(this.outputDir, autoConfigurationClasses.module + ".adoc")))) {
+ writer.println("[cols=\"4,1\"]");
+ writer.println("|===");
+ writer.println("| Configuration Class | Links");
+
+ for (AutoConfigurationClass autoConfigurationClass : autoConfigurationClasses.classes) {
+ writer.println();
+ writer.printf("| {spring-boot-code}/spring-boot-project/%s/src/main/java/%s.java[`%s`]%n",
+ autoConfigurationClasses.module, autoConfigurationClass.path, autoConfigurationClass.name);
+ writer.printf("| {spring-boot-api}/%s.html[javadoc]%n", autoConfigurationClass.path);
+ }
+
+ writer.println("|===");
+ }
+ }
+
+ private static final class AutoConfiguration {
+
+ private final String module;
+
+ private final SortedSet classes;
+
+ private AutoConfiguration(String module, Set classNames) {
+ this.module = module;
+ this.classes = classNames.stream().map((className) -> {
+ String path = className.replace('.', '/');
+ String name = className.substring(className.lastIndexOf('.') + 1);
+ return new AutoConfigurationClass(name, path);
+ }).collect(Collectors.toCollection(TreeSet::new));
+ }
+
+ }
+
+ private static final class AutoConfigurationClass implements Comparable {
+
+ private final String name;
+
+ private final String path;
+
+ private AutoConfigurationClass(String name, String path) {
+ this.name = name;
+ this.path = path;
+ }
+
+ @Override
+ public int compareTo(AutoConfigurationClass other) {
+ return this.name.compareTo(other.name);
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java
new file mode 100644
index 0000000000..e524749a0d
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2012-2020 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.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import groovy.lang.Closure;
+import groovy.lang.GroovyObjectSupport;
+import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException;
+import org.apache.maven.artifact.versioning.VersionRange;
+import org.gradle.api.InvalidUserCodeException;
+import org.gradle.api.InvalidUserDataException;
+import org.gradle.api.artifacts.dsl.DependencyHandler;
+import org.gradle.util.ConfigureUtil;
+
+import org.springframework.boot.build.bom.Library.Exclusion;
+import org.springframework.boot.build.bom.Library.Group;
+import org.springframework.boot.build.bom.Library.Module;
+import org.springframework.boot.build.bom.Library.ProhibitedVersion;
+import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
+
+/**
+ * DSL extensions for {@link BomPlugin}.
+ *
+ * @author Andy Wilkinson
+ */
+public class BomExtension {
+
+ private final Map properties = new LinkedHashMap<>();
+
+ private final Map artifactVersionProperties = new HashMap<>();
+
+ private final List libraries = new ArrayList();
+
+ private final DependencyHandler dependencyHandler;
+
+ private final UpgradeHandler upgradeHandler = new UpgradeHandler();
+
+ public BomExtension(DependencyHandler dependencyHandler) {
+ this.dependencyHandler = dependencyHandler;
+ }
+
+ public List getLibraries() {
+ return this.libraries;
+ }
+
+ public void upgrade(Closure> closure) {
+ ConfigureUtil.configure(closure, this.upgradeHandler);
+ }
+
+ public Upgrade getUpgrade() {
+ return new Upgrade(this.upgradeHandler.upgradePolicy, new GitHub(this.upgradeHandler.gitHub.organization,
+ this.upgradeHandler.gitHub.repository, this.upgradeHandler.gitHub.issueLabels));
+ }
+
+ public void library(String name, String version, Closure> closure) {
+ LibraryHandler libraryHandler = new LibraryHandler();
+ ConfigureUtil.configure(closure, libraryHandler);
+ addLibrary(new Library(name, DependencyVersion.parse(version), libraryHandler.groups,
+ libraryHandler.prohibitedVersions));
+ }
+
+ private String createDependencyNotation(String groupId, String artifactId, DependencyVersion version) {
+ return groupId + ":" + artifactId + ":" + version;
+ }
+
+ Map getProperties() {
+ return this.properties;
+ }
+
+ String getArtifactVersionProperty(String groupId, String artifactId) {
+ String coordinates = groupId + ":" + artifactId;
+ return this.artifactVersionProperties.get(coordinates);
+ }
+
+ private void putArtifactVersionProperty(String groupId, String artifactId, String versionProperty) {
+ String coordinates = groupId + ":" + artifactId;
+ String existing = this.artifactVersionProperties.putIfAbsent(coordinates, versionProperty);
+ if (existing != null) {
+ throw new InvalidUserDataException("Cannot put version property for '" + coordinates
+ + "'. Version property '" + existing + "' has already been stored.");
+ }
+ }
+
+ private void addLibrary(Library library) {
+ this.libraries.add(library);
+ this.properties.put(library.getVersionProperty(), library.getVersion());
+ for (Group group : library.getGroups()) {
+ for (Module module : group.getModules()) {
+ this.putArtifactVersionProperty(group.getId(), module.getName(), library.getVersionProperty());
+ this.dependencyHandler.getConstraints().add("api",
+ createDependencyNotation(group.getId(), module.getName(), library.getVersion()));
+ }
+ for (String bomImport : group.getBoms()) {
+ this.putArtifactVersionProperty(group.getId(), bomImport, library.getVersionProperty());
+ this.dependencyHandler.add("api", this.dependencyHandler
+ .enforcedPlatform(createDependencyNotation(group.getId(), bomImport, library.getVersion())));
+ }
+ }
+ }
+
+ public static class LibraryHandler {
+
+ private final List groups = new ArrayList<>();
+
+ private final List prohibitedVersions = new ArrayList<>();
+
+ public void group(String id, Closure> closure) {
+ GroupHandler groupHandler = new GroupHandler(id);
+ ConfigureUtil.configure(closure, groupHandler);
+ this.groups
+ .add(new Group(groupHandler.id, groupHandler.modules, groupHandler.plugins, groupHandler.imports));
+ }
+
+ public void prohibit(String range, Closure> closure) {
+ ProhibitedVersionHandler prohibitedVersionHandler = new ProhibitedVersionHandler();
+ ConfigureUtil.configure(closure, prohibitedVersionHandler);
+ try {
+ this.prohibitedVersions.add(new ProhibitedVersion(VersionRange.createFromVersionSpec(range),
+ prohibitedVersionHandler.reason));
+ }
+ catch (InvalidVersionSpecificationException ex) {
+ throw new InvalidUserCodeException("Invalid version range", ex);
+ }
+ }
+
+ public static class ProhibitedVersionHandler {
+
+ private String reason;
+
+ public void because(String because) {
+ this.reason = because;
+ }
+
+ }
+
+ public class GroupHandler extends GroovyObjectSupport {
+
+ private final String id;
+
+ private List modules = new ArrayList<>();
+
+ private List imports = new ArrayList<>();
+
+ private List plugins = new ArrayList<>();
+
+ public GroupHandler(String id) {
+ this.id = id;
+ }
+
+ public void setModules(List modules) {
+ this.modules = modules.stream()
+ .map((input) -> (input instanceof Module) ? (Module) input : new Module((String) input))
+ .collect(Collectors.toList());
+ }
+
+ public void setImports(List imports) {
+ this.imports = imports;
+ }
+
+ public void setPlugins(List plugins) {
+ this.plugins = plugins;
+ }
+
+ public Object methodMissing(String name, Object args) {
+ if (args instanceof Object[] && ((Object[]) args).length == 1) {
+ Object arg = ((Object[]) args)[0];
+ if (arg instanceof Closure) {
+ ExclusionHandler exclusionHandler = new ExclusionHandler();
+ ConfigureUtil.configure((Closure>) arg, exclusionHandler);
+ return new Module(name, exclusionHandler.exclusions);
+ }
+ }
+ throw new InvalidUserDataException("Invalid exclusion configuration for module '" + name + "'");
+ }
+
+ public class ExclusionHandler {
+
+ private final List exclusions = new ArrayList<>();
+
+ public void exclude(Map exclusion) {
+ this.exclusions.add(new Exclusion(exclusion.get("group"), exclusion.get("module")));
+ }
+
+ }
+
+ }
+
+ }
+
+ public static class UpgradeHandler {
+
+ private UpgradePolicy upgradePolicy;
+
+ private final GitHubHandler gitHub = new GitHubHandler();
+
+ public void setPolicy(UpgradePolicy upgradePolicy) {
+ this.upgradePolicy = upgradePolicy;
+ }
+
+ public void gitHub(Closure> closure) {
+ ConfigureUtil.configure(closure, this.gitHub);
+ }
+
+ }
+
+ public static final class Upgrade {
+
+ private final UpgradePolicy upgradePolicy;
+
+ private final GitHub gitHub;
+
+ private Upgrade(UpgradePolicy upgradePolicy, GitHub gitHub) {
+ this.upgradePolicy = upgradePolicy;
+ this.gitHub = gitHub;
+ }
+
+ public UpgradePolicy getPolicy() {
+ return this.upgradePolicy;
+ }
+
+ public GitHub getGitHub() {
+ return this.gitHub;
+ }
+
+ }
+
+ public static class GitHubHandler {
+
+ private String organization = "spring-projects";
+
+ private String repository = "spring-boot";
+
+ private List issueLabels;
+
+ public void setOrganization(String organization) {
+ this.organization = organization;
+ }
+
+ public void setRepository(String repository) {
+ this.repository = repository;
+ }
+
+ public void setIssueLabels(List issueLabels) {
+ this.issueLabels = issueLabels;
+ }
+
+ }
+
+ public static final class GitHub {
+
+ private String organization = "spring-projects";
+
+ private String repository = "spring-boot";
+
+ private List issueLabels;
+
+ private GitHub(String organization, String repository, List issueLabels) {
+ this.organization = organization;
+ this.repository = repository;
+ this.issueLabels = issueLabels;
+ }
+
+ public String getOrganization() {
+ return this.organization;
+ }
+
+ public String getRepository() {
+ return this.repository;
+ }
+
+ public List getIssueLabels() {
+ return this.issueLabels;
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java
new file mode 100644
index 0000000000..9b08ede52d
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomPlugin.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2012-2020 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.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathFactory;
+
+import groovy.util.Node;
+import groovy.xml.QName;
+import org.gradle.api.Action;
+import org.gradle.api.GradleException;
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.Task;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.plugins.JavaPlatformExtension;
+import org.gradle.api.plugins.JavaPlatformPlugin;
+import org.gradle.api.plugins.PluginContainer;
+import org.gradle.api.publish.PublishingExtension;
+import org.gradle.api.publish.maven.MavenPom;
+import org.gradle.api.publish.maven.MavenPublication;
+import org.gradle.api.publish.maven.tasks.GenerateMavenPom;
+import org.gradle.api.tasks.Sync;
+import org.gradle.api.tasks.TaskExecutionException;
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+
+import org.springframework.boot.build.DeployedPlugin;
+import org.springframework.boot.build.MavenRepositoryPlugin;
+import org.springframework.boot.build.bom.Library.Group;
+import org.springframework.boot.build.bom.bomr.UpgradeBom;
+import org.springframework.boot.build.mavenplugin.MavenExec;
+import org.springframework.util.FileCopyUtils;
+
+/**
+ * {@link Plugin} for defining a bom. Dependencies are added as constraints in the
+ * {@code api} configuration. Imported boms are added as enforced platforms in the
+ * {@code api} configuration.
+ *
+ * @author Andy Wilkinson
+ */
+public class BomPlugin implements Plugin {
+
+ @Override
+ public void apply(Project project) {
+ PluginContainer plugins = project.getPlugins();
+ plugins.apply(DeployedPlugin.class);
+ plugins.apply(MavenRepositoryPlugin.class);
+ plugins.apply(JavaPlatformPlugin.class);
+ JavaPlatformExtension javaPlatform = project.getExtensions().getByType(JavaPlatformExtension.class);
+ javaPlatform.allowDependencies();
+ BomExtension bom = project.getExtensions().create("bom", BomExtension.class, project.getDependencies());
+ project.getTasks().create("bomrCheck", CheckBom.class, bom);
+ project.getTasks().create("bomrUpgrade", UpgradeBom.class, bom);
+ new PublishingCustomizer(project, bom).customize();
+ Configuration effectiveBomConfiguration = project.getConfigurations().create("effectiveBom");
+ project.getTasks().matching((task) -> task.getName().equals(DeployedPlugin.GENERATE_POM_TASK_NAME))
+ .all((task) -> {
+ Sync syncBom = project.getTasks().create("syncBom", Sync.class);
+ syncBom.dependsOn(task);
+ File generatedBomDir = new File(project.getBuildDir(), "generated/bom");
+ syncBom.setDestinationDir(generatedBomDir);
+ syncBom.from(((GenerateMavenPom) task).getDestination(), (pom) -> pom.rename((name) -> "pom.xml"));
+ try {
+ String settingsXmlContent = FileCopyUtils
+ .copyToString(new InputStreamReader(
+ getClass().getClassLoader().getResourceAsStream("effective-bom-settings.xml"),
+ StandardCharsets.UTF_8))
+ .replace("localRepositoryPath",
+ new File(project.getBuildDir(), "local-m2-repository").getAbsolutePath());
+ syncBom.from(project.getResources().getText().fromString(settingsXmlContent),
+ (settingsXml) -> settingsXml.rename((name) -> "settings.xml"));
+ }
+ catch (IOException ex) {
+ throw new GradleException("Failed to prepare settings.xml", ex);
+ }
+ MavenExec generateEffectiveBom = project.getTasks().create("generateEffectiveBom", MavenExec.class);
+ generateEffectiveBom.setProjectDir(generatedBomDir);
+ File effectiveBom = new File(project.getBuildDir(),
+ "generated/effective-bom/" + project.getName() + "-effective-bom.xml");
+ generateEffectiveBom.args("--settings", "settings.xml", "help:effective-pom",
+ "-Doutput=" + effectiveBom);
+ generateEffectiveBom.dependsOn(syncBom);
+ generateEffectiveBom.getOutputs().file(effectiveBom);
+ generateEffectiveBom.doLast(new StripUnrepeatableOutputAction(effectiveBom));
+ project.getArtifacts().add(effectiveBomConfiguration.getName(), effectiveBom,
+ (artifact) -> artifact.builtBy(generateEffectiveBom));
+ });
+ }
+
+ private static final class PublishingCustomizer {
+
+ private final Project project;
+
+ private final BomExtension bom;
+
+ private PublishingCustomizer(Project project, BomExtension bom) {
+ this.project = project;
+ this.bom = bom;
+ }
+
+ private void customize() {
+ PublishingExtension publishing = this.project.getExtensions().getByType(PublishingExtension.class);
+ publishing.getPublications().withType(MavenPublication.class).all(this::configurePublication);
+ }
+
+ private void configurePublication(MavenPublication publication) {
+ publication.pom(this::customizePom);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void customizePom(MavenPom pom) {
+ pom.withXml((xml) -> {
+ Node projectNode = xml.asNode();
+ Node properties = new Node(null, "properties");
+ this.bom.getProperties().forEach(properties::appendNode);
+ Node dependencyManagement = findChild(projectNode, "dependencyManagement");
+ if (dependencyManagement != null) {
+ addPropertiesBeforeDependencyManagement(projectNode, properties);
+ replaceVersionsWithVersionPropertyReferences(dependencyManagement);
+ addExclusionsToManagedDependencies(dependencyManagement);
+ }
+ else {
+ projectNode.children().add(properties);
+ }
+ addPluginManagement(projectNode);
+ });
+ }
+
+ @SuppressWarnings("unchecked")
+ private void addPropertiesBeforeDependencyManagement(Node projectNode, Node properties) {
+ for (int i = 0; i < projectNode.children().size(); i++) {
+ if (isNodeWithName(projectNode.children().get(i), "dependencyManagement")) {
+ projectNode.children().add(i, properties);
+ break;
+ }
+ }
+ }
+
+ private void replaceVersionsWithVersionPropertyReferences(Node dependencyManagement) {
+ Node dependencies = findChild(dependencyManagement, "dependencies");
+ if (dependencies != null) {
+ for (Node dependency : findChildren(dependencies, "dependency")) {
+ String groupId = findChild(dependency, "groupId").text();
+ String artifactId = findChild(dependency, "artifactId").text();
+ findChild(dependency, "version")
+ .setValue("${" + this.bom.getArtifactVersionProperty(groupId, artifactId) + "}");
+ }
+ }
+ }
+
+ private void addExclusionsToManagedDependencies(Node dependencyManagement) {
+ Node dependencies = findChild(dependencyManagement, "dependencies");
+ if (dependencies != null) {
+ for (Node dependency : findChildren(dependencies, "dependency")) {
+ String groupId = findChild(dependency, "groupId").text();
+ String artifactId = findChild(dependency, "artifactId").text();
+ this.bom.getLibraries().stream().flatMap((library) -> library.getGroups().stream())
+ .filter((group) -> group.getId().equals(groupId))
+ .flatMap((group) -> group.getModules().stream())
+ .filter((module) -> module.getName().equals(artifactId))
+ .flatMap((module) -> module.getExclusions().stream()).forEach((exclusion) -> {
+ Node exclusions = findOrCreateNode(dependency, "exclusions");
+ Node node = new Node(exclusions, "exclusion");
+ node.appendNode("groupId", exclusion.getGroupId());
+ node.appendNode("artifactId", exclusion.getArtifactId());
+ });
+ }
+ }
+ }
+
+ private void addPluginManagement(Node projectNode) {
+ for (Library library : this.bom.getLibraries()) {
+ for (Group group : library.getGroups()) {
+ Node plugins = findOrCreateNode(projectNode, "build", "pluginManagement", "plugins");
+ for (String pluginName : group.getPlugins()) {
+ Node plugin = new Node(plugins, "plugin");
+ plugin.appendNode("groupId", group.getId());
+ plugin.appendNode("artifactId", pluginName);
+ plugin.appendNode("version", "${" + library.getVersionProperty() + "}");
+ }
+ }
+ }
+ }
+
+ private Node findOrCreateNode(Node parent, String... path) {
+ Node current = parent;
+ for (String nodeName : path) {
+ Node child = findChild(current, nodeName);
+ if (child == null) {
+ child = new Node(current, nodeName);
+ }
+ current = child;
+ }
+ return current;
+ }
+
+ private Node findChild(Node parent, String name) {
+ for (Object child : parent.children()) {
+ if (child instanceof Node) {
+ Node node = (Node) child;
+ if ((node.name() instanceof QName) && name.equals(((QName) node.name()).getLocalPart())) {
+ return node;
+ }
+ if (name.equals(node.name())) {
+ return node;
+ }
+ }
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private List findChildren(Node parent, String name) {
+ return (List) parent.children().stream().filter((child) -> isNodeWithName(child, name))
+ .collect(Collectors.toList());
+
+ }
+
+ private boolean isNodeWithName(Object candidate, String name) {
+ if (candidate instanceof Node) {
+ Node node = (Node) candidate;
+ if ((node.name() instanceof QName) && name.equals(((QName) node.name()).getLocalPart())) {
+ return true;
+ }
+ if (name.equals(node.name())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ }
+
+ private static final class StripUnrepeatableOutputAction implements Action {
+
+ private final File effectiveBom;
+
+ private StripUnrepeatableOutputAction(File xmlFile) {
+ this.effectiveBom = xmlFile;
+ }
+
+ @Override
+ public void execute(Task task) {
+ try {
+ Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(this.effectiveBom);
+ XPath xpath = XPathFactory.newInstance().newXPath();
+ NodeList comments = (NodeList) xpath.evaluate("//comment()", document, XPathConstants.NODESET);
+ for (int i = 0; i < comments.getLength(); i++) {
+ org.w3c.dom.Node comment = comments.item(i);
+ comment.getParentNode().removeChild(comment);
+ }
+ org.w3c.dom.Node build = (org.w3c.dom.Node) xpath.evaluate("/project/build", document,
+ XPathConstants.NODE);
+ build.getParentNode().removeChild(build);
+ org.w3c.dom.Node reporting = (org.w3c.dom.Node) xpath.evaluate("/project/reporting", document,
+ XPathConstants.NODE);
+ reporting.getParentNode().removeChild(reporting);
+ TransformerFactory.newInstance().newTransformer().transform(new DOMSource(document),
+ new StreamResult(this.effectiveBom));
+ }
+ catch (Exception ex) {
+ throw new TaskExecutionException(task, ex);
+ }
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java
new file mode 100644
index 0000000000..cde5d53513
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/CheckBom.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2012-2020 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.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+
+import org.gradle.api.DefaultTask;
+import org.gradle.api.InvalidUserDataException;
+import org.gradle.api.tasks.TaskAction;
+
+import org.springframework.boot.build.bom.Library.Group;
+import org.springframework.boot.build.bom.Library.Module;
+import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
+
+/**
+ * Checks the validity of a bom.
+ *
+ * @author Andy Wilkinson
+ */
+public class CheckBom extends DefaultTask {
+
+ private final BomExtension bom;
+
+ @Inject
+ public CheckBom(BomExtension bom) {
+ this.bom = bom;
+ }
+
+ @TaskAction
+ void checkBom() {
+ for (Library library : this.bom.getLibraries()) {
+ for (Group group : library.getGroups()) {
+ for (Module module : group.getModules()) {
+ if (!module.getExclusions().isEmpty()) {
+ checkExclusions(group.getId(), module, library.getVersion());
+ }
+ }
+ }
+ }
+ }
+
+ private void checkExclusions(String groupId, Module module, DependencyVersion version) {
+ Set resolved = getProject().getConfigurations()
+ .detachedConfiguration(
+ getProject().getDependencies().create(groupId + ":" + module.getName() + ":" + version))
+ .getResolvedConfiguration().getResolvedArtifacts().stream()
+ .map((artifact) -> artifact.getModuleVersion().getId())
+ .map((id) -> id.getGroup() + ":" + id.getModule().getName()).collect(Collectors.toSet());
+ Set exclusions = module.getExclusions().stream()
+ .map((exclusion) -> exclusion.getGroupId() + ":" + exclusion.getArtifactId())
+ .collect(Collectors.toSet());
+ Set unused = new TreeSet<>();
+ for (String exclusion : exclusions) {
+ if (!resolved.contains(exclusion) && exclusion.endsWith(":*")) {
+ String group = exclusion.substring(0, exclusion.indexOf(':') + 1);
+ if (!resolved.stream().filter((candidate) -> candidate.startsWith(group)).findFirst().isPresent()) {
+ unused.add(exclusion);
+ }
+ }
+ }
+ exclusions.removeAll(resolved);
+ if (!unused.isEmpty()) {
+ throw new InvalidUserDataException(
+ "Unnecessary exclusions on " + groupId + ":" + module.getName() + ": " + exclusions);
+ }
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java
new file mode 100644
index 0000000000..e28447838c
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2012-2020 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.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.maven.artifact.versioning.VersionRange;
+
+import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
+
+/**
+ * A collection of modules, Maven plugins, and Maven boms that are versioned and released
+ * together.
+ *
+ * @author Andy Wilkinson
+ */
+public class Library {
+
+ private final String name;
+
+ private final DependencyVersion version;
+
+ private final List groups;
+
+ private final String versionProperty;
+
+ private final List prohibitedVersions;
+
+ /**
+ * Create a new {@code Library} with the given {@code name}, {@code version}, and
+ * {@code groups}.
+ * @param name name of the library
+ * @param version version of the library
+ * @param groups groups in the library
+ * @param prohibitedVersions version of the library that are prohibited
+ */
+ public Library(String name, DependencyVersion version, List groups,
+ List prohibitedVersions) {
+ this.name = name;
+ this.version = version;
+ this.groups = groups;
+ this.versionProperty = name.toLowerCase(Locale.ENGLISH).replace(' ', '-') + ".version";
+ this.prohibitedVersions = prohibitedVersions;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public DependencyVersion getVersion() {
+ return this.version;
+ }
+
+ public List getGroups() {
+ return this.groups;
+ }
+
+ public String getVersionProperty() {
+ return this.versionProperty;
+ }
+
+ public List getProhibitedVersions() {
+ return this.prohibitedVersions;
+ }
+
+ /**
+ * A version or range of versions that are prohibited from being used in a bom.
+ */
+ public static class ProhibitedVersion {
+
+ private final VersionRange range;
+
+ private final String reason;
+
+ public ProhibitedVersion(VersionRange range, String reason) {
+ this.range = range;
+ this.reason = reason;
+ }
+
+ public VersionRange getRange() {
+ return this.range;
+ }
+
+ public String getReason() {
+ return this.reason;
+ }
+
+ }
+
+ /**
+ * A collection of modules, Maven plugins, and Maven boms with the same group ID.
+ */
+ public static class Group {
+
+ private final String id;
+
+ private final List modules;
+
+ private final List plugins;
+
+ private final List boms;
+
+ public Group(String id, List modules, List plugins, List boms) {
+ this.id = id;
+ this.modules = modules;
+ this.plugins = plugins;
+ this.boms = boms;
+ }
+
+ public String getId() {
+ return this.id;
+ }
+
+ public List getModules() {
+ return this.modules;
+ }
+
+ public List getPlugins() {
+ return this.plugins;
+ }
+
+ public List getBoms() {
+ return this.boms;
+ }
+
+ }
+
+ /**
+ * A module in a group.
+ */
+ public static class Module {
+
+ private final String name;
+
+ private final List exclusions;
+
+ public Module(String name) {
+ this(name, Collections.emptyList());
+ }
+
+ public Module(String name, List exclusions) {
+ this.name = name;
+ this.exclusions = exclusions;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public List getExclusions() {
+ return this.exclusions;
+ }
+
+ }
+
+ /**
+ * An exclusion of a dependency identified by its group ID and artifact ID.
+ */
+ public static class Exclusion {
+
+ private final String groupId;
+
+ private final String artifactId;
+
+ public Exclusion(String groupId, String artifactId) {
+ this.groupId = groupId;
+ this.artifactId = artifactId;
+ }
+
+ public String getGroupId() {
+ return this.groupId;
+ }
+
+ public String getArtifactId() {
+ return this.artifactId;
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/UpgradePolicy.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/UpgradePolicy.java
new file mode 100644
index 0000000000..831d3b4f85
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/UpgradePolicy.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2012-2020 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.util.function.BiPredicate;
+
+import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
+
+/**
+ * Policies used to decide which versions are considered as possible upgrades.
+ *
+ * @author Andy Wilkinson
+ */
+public enum UpgradePolicy implements BiPredicate {
+
+ /**
+ * All versions more recent than the current version will be suggested as possible
+ * upgrades.
+ */
+ ANY((candidate, current) -> current.compareTo(candidate) < 0),
+
+ /**
+ * New minor versions of the current major version will be suggested as possible
+ * upgrades. For example, if the current version is 1.2.3, all 1.x.y versions after
+ * 1.2.3 will be suggested. 2.x versions will not be offered.
+ */
+ SAME_MAJOR_VERSION(DependencyVersion::isSameMajorAndNewerThan),
+
+ /**
+ * New patch versions of the current minor version will be offered as possible
+ * upgrades. For example, if the current version is 1.2.3, all 1.2.x versions after
+ * 1.2.3 will be suggested. 1.x versions will not be offered.
+ */
+ SAME_MINOR_VERSION(DependencyVersion::isSameMinorAndNewerThan);
+
+ private BiPredicate delegate;
+
+ UpgradePolicy(BiPredicate delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean test(DependencyVersion candidate, DependencyVersion current) {
+ return this.delegate.test(candidate, current);
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/InteractiveUpgradeResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/InteractiveUpgradeResolver.java
new file mode 100644
index 0000000000..b216e9e382
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/InteractiveUpgradeResolver.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2012-2020 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.bomr;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+import java.util.stream.Collectors;
+
+import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
+import org.gradle.api.internal.tasks.userinput.UserInputHandler;
+
+import org.springframework.boot.build.bom.Library;
+import org.springframework.boot.build.bom.Library.Group;
+import org.springframework.boot.build.bom.Library.Module;
+import org.springframework.boot.build.bom.Library.ProhibitedVersion;
+import org.springframework.boot.build.bom.UpgradePolicy;
+import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
+import org.springframework.util.StringUtils;
+
+/**
+ * Interactive {@link UpgradeResolver} that uses command line input to choose the upgrades
+ * to apply.
+ *
+ * @author Andy Wilkinson
+ */
+public final class InteractiveUpgradeResolver implements UpgradeResolver {
+
+ private final VersionResolver versionResolver;
+
+ private final UpgradePolicy upgradePolicy;
+
+ private final UserInputHandler userInputHandler;
+
+ InteractiveUpgradeResolver(VersionResolver versionResolver, UpgradePolicy upgradePolicy,
+ UserInputHandler userInputHandler) {
+ this.versionResolver = versionResolver;
+ this.upgradePolicy = upgradePolicy;
+ this.userInputHandler = userInputHandler;
+ }
+
+ @Override
+ public List resolveUpgrades(Collection libraries) {
+ return libraries.stream().map(this::resolveUpgrade).filter((upgrade) -> upgrade != null)
+ .collect(Collectors.toList());
+ }
+
+ private Upgrade resolveUpgrade(Library library) {
+ Map> moduleVersions = new LinkedHashMap<>();
+ for (Group group : library.getGroups()) {
+ for (Module module : group.getModules()) {
+ moduleVersions.put(group.getId() + ":" + module.getName(),
+ getLaterVersionsForModule(group.getId(), module.getName(), library.getVersion()));
+ }
+ }
+ List allVersions = moduleVersions.values().stream().flatMap(SortedSet::stream).distinct()
+ .filter((dependencyVersion) -> isPermitted(dependencyVersion, library.getProhibitedVersions()))
+ .collect(Collectors.toList());
+ if (allVersions.isEmpty()) {
+ return null;
+ }
+ List versionOptions = allVersions.stream()
+ .map((version) -> new VersionOption(version, getMissingModules(moduleVersions, version)))
+ .collect(Collectors.toList());
+ VersionOption current = new VersionOption(library.getVersion(), Collections.emptyList());
+ VersionOption selected = this.userInputHandler.selectOption(library.getName() + " " + library.getVersion(),
+ versionOptions, current);
+ return (selected.equals(current)) ? null : new Upgrade(library, selected.version);
+ }
+
+ private boolean isPermitted(DependencyVersion dependencyVersion, List prohibitedVersions) {
+ if (prohibitedVersions.isEmpty()) {
+ return true;
+ }
+ for (ProhibitedVersion prohibitedVersion : prohibitedVersions) {
+ if (prohibitedVersion.getRange()
+ .containsVersion(new DefaultArtifactVersion(dependencyVersion.toString()))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private List getMissingModules(Map> moduleVersions,
+ DependencyVersion version) {
+ List missingModules = new ArrayList<>();
+ moduleVersions.forEach((name, versions) -> {
+ if (!versions.contains(version)) {
+ missingModules.add(name);
+ }
+ });
+ return missingModules;
+ }
+
+ private SortedSet getLaterVersionsForModule(String groupId, String artifactId,
+ DependencyVersion currentVersion) {
+ SortedSet versions = this.versionResolver.resolveVersions(groupId, artifactId);
+ versions.removeIf((candidate) -> !this.upgradePolicy.test(candidate, currentVersion));
+ return versions;
+ }
+
+ private static final class VersionOption {
+
+ private final DependencyVersion version;
+
+ private final List missingModules;
+
+ private VersionOption(DependencyVersion version, List missingModules) {
+ this.version = version;
+ this.missingModules = missingModules;
+ }
+
+ @Override
+ public String toString() {
+ if (this.missingModules.isEmpty()) {
+ return this.version.toString();
+ }
+ return this.version + " (some modules are missing: "
+ + StringUtils.collectionToDelimitedString(this.missingModules, ", ") + ")";
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MavenMetadataVersionResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MavenMetadataVersionResolver.java
new file mode 100644
index 0000000000..d29f6b0759
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/MavenMetadataVersionResolver.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2012-2020 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.bomr;
+
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathFactory;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+
+import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.converter.StringHttpMessageConverter;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * A {@link VersionResolver} that examines {@code maven-metadata.xml} to determine the
+ * available versions.
+ *
+ * @author Andy Wilkinson
+ */
+final class MavenMetadataVersionResolver implements VersionResolver {
+
+ private final RestTemplate rest;
+
+ private final List repositoryUrls;
+
+ MavenMetadataVersionResolver(List repositoryUrls) {
+ this(new RestTemplate(Arrays.asList(new StringHttpMessageConverter())), repositoryUrls);
+ }
+
+ MavenMetadataVersionResolver(RestTemplate restTemplate, List repositoryUrls) {
+ this.rest = restTemplate;
+ this.repositoryUrls = repositoryUrls;
+ }
+
+ @Override
+ public SortedSet resolveVersions(String groupId, String artifactId) {
+ Set versions = new HashSet();
+ for (String repositoryUrl : this.repositoryUrls) {
+ versions.addAll(resolveVersions(groupId, artifactId, repositoryUrl));
+ }
+ return new TreeSet<>(versions.stream().map(DependencyVersion::parse).collect(Collectors.toSet()));
+ }
+
+ private Set resolveVersions(String groupId, String artifactId, String repositoryUrl) {
+ Set versions = new HashSet();
+ String url = repositoryUrl + "/" + groupId.replace('.', '/') + "/" + artifactId + "/maven-metadata.xml";
+ try {
+ String metadata = this.rest.getForObject(url, String.class);
+ Document metadataDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder()
+ .parse(new InputSource(new StringReader(metadata)));
+ NodeList versionNodes = (NodeList) XPathFactory.newInstance().newXPath()
+ .evaluate("/metadata/versioning/versions/version", metadataDocument, XPathConstants.NODESET);
+ for (int i = 0; i < versionNodes.getLength(); i++) {
+ versions.add(versionNodes.item(i).getTextContent());
+ }
+ }
+ catch (HttpClientErrorException ex) {
+ if (ex.getStatusCode() != HttpStatus.NOT_FOUND) {
+ System.err.println("Failed to download maven-metadata.xml for " + groupId + ":" + artifactId + " from "
+ + url + ": " + ex.getMessage());
+ }
+ }
+ catch (Exception ex) {
+ System.err.println("Failed to resolve versions for module " + groupId + ":" + artifactId + " in repository "
+ + repositoryUrl + ": " + ex.getMessage());
+ }
+ return versions;
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/Upgrade.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/Upgrade.java
new file mode 100644
index 0000000000..e0457fe8f4
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/Upgrade.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-2020 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.bomr;
+
+import org.springframework.boot.build.bom.Library;
+import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
+
+/**
+ * An upgrade to change a {@link Library} to use a new version}.
+ *
+ * @author Andy Wilkinson
+ */
+final class Upgrade {
+
+ private final Library library;
+
+ private final DependencyVersion version;
+
+ Upgrade(Library library, DependencyVersion version) {
+ this.library = library;
+ this.version = version;
+ }
+
+ Library getLibrary() {
+ return this.library;
+ }
+
+ DependencyVersion getVersion() {
+ return this.version;
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeBom.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeBom.java
new file mode 100644
index 0000000000..7da198a5f1
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeBom.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2012-2020 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.bomr;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+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.Optional;
+import java.util.Properties;
+
+import javax.inject.Inject;
+
+import org.gradle.api.DefaultTask;
+import org.gradle.api.InvalidUserDataException;
+import org.gradle.api.Task;
+import org.gradle.api.internal.tasks.userinput.UserInputHandler;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.api.tasks.TaskExecutionException;
+import org.gradle.api.tasks.options.Option;
+
+import org.springframework.boot.build.bom.BomExtension;
+import org.springframework.boot.build.bom.bomr.github.GitHub;
+import org.springframework.boot.build.bom.bomr.github.GitHubRepository;
+import org.springframework.boot.build.bom.bomr.github.Milestone;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link Task} to upgrade the libraries managed by a bom.
+ *
+ * @author Andy Wilkinson
+ */
+public class UpgradeBom extends DefaultTask {
+
+ private final BomExtension bom;
+
+ private String milestone;
+
+ @Inject
+ public UpgradeBom(BomExtension bom) {
+ this.bom = bom;
+ }
+
+ @Option(option = "milestone", description = "Milestone to which dependency upgrade issues should be assigned")
+ public void setMilestone(String milestone) {
+ this.milestone = milestone;
+ }
+
+ @TaskAction
+ void upgradeDependencies() {
+ GitHubRepository repository = createGitHub().getRepository(this.bom.getUpgrade().getGitHub().getOrganization(),
+ this.bom.getUpgrade().getGitHub().getRepository());
+ List availableLabels = repository.getLabels();
+ List issueLabels = this.bom.getUpgrade().getGitHub().getIssueLabels();
+ if (!availableLabels.containsAll(issueLabels)) {
+ List unknownLabels = new ArrayList<>(issueLabels);
+ unknownLabels.removeAll(availableLabels);
+ throw new InvalidUserDataException(
+ "Unknown label(s): " + StringUtils.collectionToCommaDelimitedString(unknownLabels));
+ }
+ Milestone milestone = determineMilestone(repository);
+ List upgrades = new InteractiveUpgradeResolver(
+ new MavenMetadataVersionResolver(Arrays.asList("https://repo1.maven.org/maven2/")),
+ this.bom.getUpgrade().getPolicy(), getServices().get(UserInputHandler.class))
+ .resolveUpgrades(this.bom.getLibraries());
+ for (Upgrade upgrade : upgrades) {
+ String title = "Upgrade to " + upgrade.getLibrary().getName() + " " + upgrade.getVersion();
+ System.out.println(title);
+ try {
+ Path buildFile = getProject().getBuildFile().toPath();
+ applyChanges(upgrade, buildFile);
+ int issueNumber = repository.openIssue(title, issueLabels, milestone);
+ if (new ProcessBuilder().command("git", "add", buildFile.toFile().getAbsolutePath()).start()
+ .waitFor() != 0) {
+ throw new IllegalStateException("git add failed");
+ }
+ if (new ProcessBuilder().command("git", "commit", "-m", title + "\n\nCloses gh-" + issueNumber).start()
+ .waitFor() != 0) {
+ throw new IllegalStateException("git commit failed");
+ }
+ }
+ catch (IOException ex) {
+ throw new TaskExecutionException(this, ex);
+ }
+ catch (InterruptedException ex) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ private GitHub createGitHub() {
+ Properties bomrProperties = new Properties();
+ try (Reader reader = new FileReader(new File(System.getProperty("user.home"), ".bomr.properties"))) {
+ bomrProperties.load(reader);
+ String username = bomrProperties.getProperty("bomr.github.username");
+ String password = bomrProperties.getProperty("bomr.github.password");
+ return GitHub.withCredentials(username, password);
+ }
+ catch (IOException ex) {
+ throw new InvalidUserDataException("Failed to load .bomr.properties from user home", ex);
+ }
+ }
+
+ private void applyChanges(Upgrade upgrade, Path buildFile) throws IOException {
+ String contents = new String(Files.readAllBytes(buildFile), StandardCharsets.UTF_8);
+ String modified = contents.replace(
+ "library('" + upgrade.getLibrary().getName() + "', '" + upgrade.getLibrary().getVersion() + "')",
+ "library('" + upgrade.getLibrary().getName() + "', '" + upgrade.getVersion() + "')");
+ Files.write(buildFile, modified.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE);
+ }
+
+ private Milestone determineMilestone(GitHubRepository repository) {
+ if (this.milestone == null) {
+ return null;
+ }
+ List milestones = repository.getMilestones();
+ Optional matchingMilestone = milestones.stream()
+ .filter((milestone) -> milestone.getName().equals(this.milestone)).findFirst();
+ if (!matchingMilestone.isPresent()) {
+ throw new InvalidUserDataException("Unknown milestone: " + this.milestone);
+ }
+ return matchingMilestone.get();
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeResolver.java
new file mode 100644
index 0000000000..ed1805eca0
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/UpgradeResolver.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2012-2020 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.bomr;
+
+import java.util.Collection;
+import java.util.List;
+
+import org.springframework.boot.build.bom.Library;
+
+/**
+ * Resolves upgrades for the libraries in a bom.
+ *
+ * @author Andy Wilkinson
+ */
+interface UpgradeResolver {
+
+ /**
+ * Resolves the upgrades to be applied to the given {@code libraries}.
+ * @param libraries the libraries
+ * @return the upgrades
+ */
+ List resolveUpgrades(Collection libraries);
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/VersionResolver.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/VersionResolver.java
new file mode 100644
index 0000000000..70b1c9298c
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/VersionResolver.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2012-2020 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.bomr;
+
+import java.util.SortedSet;
+
+import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
+
+/**
+ * Resolves the available versions for a module.
+ *
+ * @author Andy Wilkinson
+ */
+interface VersionResolver {
+
+ /**
+ * Resolves the available versions for the module identified by the given
+ * {@code groupId} and {@code artifactId}.
+ * @param groupId module's group ID
+ * @param artifactId module's artifact ID
+ * @return the available versions
+ */
+ SortedSet resolveVersions(String groupId, String artifactId);
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/GitHub.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/GitHub.java
new file mode 100644
index 0000000000..879caec26e
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/GitHub.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2012-2020 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.bomr.github;
+
+/**
+ * Minimal API for interacting with GitHub.
+ *
+ * @author Andy Wilkinson
+ */
+public interface GitHub {
+
+ /**
+ * Returns a {@link GitHubRepository} with the given {@code name} in the given
+ * {@code organization}.
+ * @param organization the organization
+ * @param name the name of the repository
+ * @return the repository
+ */
+ GitHubRepository getRepository(String organization, String name);
+
+ /**
+ * Creates a new {@code GitHub} that will authenticate with given {@code username} and
+ * {@code password}.
+ * @param username username for authentication
+ * @param password password for authentication
+ * @return the new {@code GitHub} instance
+ */
+ static GitHub withCredentials(String username, String password) {
+ return new StandardGitHub(username, password);
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/GitHubRepository.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/GitHubRepository.java
new file mode 100644
index 0000000000..3c49ba56d5
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/GitHubRepository.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2012-2020 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.bomr.github;
+
+import java.util.List;
+
+/**
+ * Minimal API for interacting with a GitHub repository.
+ *
+ * @author Andy Wilkinson
+ */
+public interface GitHubRepository {
+
+ /**
+ * Opens a new issue with the given title. The given {@code labels} will be applied to
+ * the issue and it will be assigned to the given {@code milestone}.
+ * @param title the title of the issue
+ * @param labels the labels to apply to the issue
+ * @param milestone the milestone to assign the issue to
+ * @return the number of the new issue
+ */
+ int openIssue(String title, List labels, Milestone milestone);
+
+ /**
+ * Returns the labels in the repository.
+ * @return the labels
+ */
+ List getLabels();
+
+ /**
+ * Returns the milestones in the repository.
+ * @return the milestones
+ */
+ List getMilestones();
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/Milestone.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/Milestone.java
new file mode 100644
index 0000000000..f50dd28c48
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/Milestone.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2012-2020 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.bomr.github;
+
+/**
+ * A milestone in a {@link GitHubRepository GitHub repository}.
+ *
+ * @author Andy Wilkinson
+ */
+public class Milestone {
+
+ private final String name;
+
+ private final int number;
+
+ Milestone(String name, int number) {
+ this.name = name;
+ this.number = number;
+ }
+
+ /**
+ * Returns the name of the milestone.
+ * @return the name
+ */
+ public String getName() {
+ return this.name;
+ }
+
+ /**
+ * Returns the number of the milestone.
+ * @return the number
+ */
+ public int getNumber() {
+ return this.number;
+ }
+
+ @Override
+ public String toString() {
+ return this.name + " (" + this.number + ")";
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/StandardGitHub.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/StandardGitHub.java
new file mode 100644
index 0000000000..5be432a0ac
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/StandardGitHub.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012-2020 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.bomr.github;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Base64;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.springframework.http.HttpRequest;
+import org.springframework.http.MediaType;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.DefaultUriBuilderFactory;
+import org.springframework.web.util.UriTemplateHandler;
+
+/**
+ * Standard implementation of {@link GitHub}.
+ *
+ * @author Andy Wilkinson
+ */
+final class StandardGitHub implements GitHub {
+
+ private final String username;
+
+ private final String password;
+
+ StandardGitHub(String username, String password) {
+ this.username = username;
+ this.password = password;
+ }
+
+ @Override
+ public GitHubRepository getRepository(String organization, String name) {
+ RestTemplate restTemplate = new RestTemplate(
+ Arrays.asList(new MappingJackson2HttpMessageConverter(new ObjectMapper())));
+ restTemplate.getInterceptors().add(new ClientHttpRequestInterceptor() {
+
+ @Override
+ public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
+ throws IOException {
+ request.getHeaders().add("User-Agent", StandardGitHub.this.username);
+ request.getHeaders().add("Authorization", "Basic " + Base64.getEncoder().encodeToString(
+ (StandardGitHub.this.username + ":" + StandardGitHub.this.password).getBytes()));
+ request.getHeaders().add("Accept", MediaType.APPLICATION_JSON_VALUE);
+ return execution.execute(request, body);
+ }
+
+ });
+ UriTemplateHandler uriTemplateHandler = new DefaultUriBuilderFactory(
+ "https://api.github.com/repos/" + organization + "/" + name + "/");
+ restTemplate.setUriTemplateHandler(uriTemplateHandler);
+ return new StandardGitHubRepository(restTemplate);
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/StandardGitHubRepository.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/StandardGitHubRepository.java
new file mode 100644
index 0000000000..6eced35271
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/github/StandardGitHubRepository.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012-2020 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.bomr.github;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Standard implementation of {@link GitHubRepository}.
+ *
+ * @author Andy Wilkinson
+ */
+final class StandardGitHubRepository implements GitHubRepository {
+
+ private final RestTemplate rest;
+
+ StandardGitHubRepository(RestTemplate restTemplate) {
+ this.rest = restTemplate;
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public int openIssue(String title, List labels, Milestone milestone) {
+ Map body = new HashMap<>();
+ body.put("title", title);
+ if (milestone != null) {
+ body.put("milestone", milestone.getNumber());
+ }
+ if (!labels.isEmpty()) {
+ body.put("labels", labels);
+ }
+ ResponseEntity response = this.rest.postForEntity("issues", body, Map.class);
+ return (Integer) response.getBody().get("number");
+ }
+
+ @Override
+ public List getLabels() {
+ return get("labels?per_page=100", (label) -> (String) label.get("name"));
+ }
+
+ @Override
+ public List getMilestones() {
+ return get("milestones?per_page=100",
+ (milestone) -> new Milestone((String) milestone.get("title"), (Integer) milestone.get("number")));
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ private List get(String name, Function, T> mapper) {
+ ResponseEntity response = this.rest.getForEntity(name, List.class);
+ List> body = response.getBody();
+ return body.stream().map(mapper).collect(Collectors.toList());
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java
new file mode 100644
index 0000000000..0192c64f6f
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/AbstractDependencyVersion.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import org.apache.maven.artifact.versioning.ComparableVersion;
+
+/**
+ * Base class for {@link DependencyVersion} implementations.
+ *
+ * @author Andy Wilkinson
+ */
+abstract class AbstractDependencyVersion implements DependencyVersion {
+
+ private final ComparableVersion comparableVersion;
+
+ protected AbstractDependencyVersion(ComparableVersion comparableVersion) {
+ this.comparableVersion = comparableVersion;
+ }
+
+ @Override
+ public int compareTo(DependencyVersion other) {
+ ComparableVersion otherComparable = (other instanceof AbstractDependencyVersion)
+ ? ((AbstractDependencyVersion) other).comparableVersion : new ComparableVersion(other.toString());
+ return this.comparableVersion.compareTo(otherComparable);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ AbstractDependencyVersion other = (AbstractDependencyVersion) obj;
+ if (!this.comparableVersion.equals(other.comparableVersion)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return this.comparableVersion.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return this.comparableVersion.toString();
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ArtifactVersionDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ArtifactVersionDependencyVersion.java
new file mode 100644
index 0000000000..8ff9408ef9
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ArtifactVersionDependencyVersion.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import java.util.Optional;
+
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.apache.maven.artifact.versioning.ComparableVersion;
+import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
+
+/**
+ * A {@link DependencyVersion} backed by an {@link ArtifactVersion}.
+ *
+ * @author Andy Wilkinson
+ */
+class ArtifactVersionDependencyVersion extends AbstractDependencyVersion {
+
+ private final ArtifactVersion artifactVersion;
+
+ protected ArtifactVersionDependencyVersion(ArtifactVersion artifactVersion) {
+ super(new ComparableVersion(artifactVersion.toString()));
+ this.artifactVersion = artifactVersion;
+ }
+
+ protected ArtifactVersionDependencyVersion(ArtifactVersion artifactVersion, ComparableVersion comparableVersion) {
+ super(comparableVersion);
+ this.artifactVersion = artifactVersion;
+ }
+
+ @Override
+ public boolean isNewerThan(DependencyVersion other) {
+ if (other instanceof ReleaseTrainDependencyVersion) {
+ return false;
+ }
+ return compareTo(other) > 0;
+ }
+
+ @Override
+ public boolean isSameMajorAndNewerThan(DependencyVersion other) {
+ if (other instanceof ReleaseTrainDependencyVersion) {
+ return false;
+ }
+ return extractArtifactVersionDependencyVersion(other).map(this::isSameMajorAndNewerThan).orElse(true);
+ }
+
+ private boolean isSameMajorAndNewerThan(ArtifactVersionDependencyVersion other) {
+ return this.artifactVersion.getMajorVersion() == other.artifactVersion.getMajorVersion() && isNewerThan(other);
+ }
+
+ @Override
+ public boolean isSameMinorAndNewerThan(DependencyVersion other) {
+ if (other instanceof ReleaseTrainDependencyVersion) {
+ return false;
+ }
+ return extractArtifactVersionDependencyVersion(other).map(this::isSameMinorAndNewerThan).orElse(true);
+ }
+
+ private boolean isSameMinorAndNewerThan(ArtifactVersionDependencyVersion other) {
+ return this.artifactVersion.getMajorVersion() == other.artifactVersion.getMajorVersion()
+ && this.artifactVersion.getMinorVersion() == other.artifactVersion.getMinorVersion()
+ && isNewerThan(other);
+ }
+
+ @Override
+ public String toString() {
+ return this.artifactVersion.toString();
+ }
+
+ private Optional extractArtifactVersionDependencyVersion(
+ DependencyVersion other) {
+ ArtifactVersionDependencyVersion artifactVersion = null;
+ if (other instanceof ArtifactVersionDependencyVersion) {
+ artifactVersion = (ArtifactVersionDependencyVersion) other;
+ }
+ return Optional.ofNullable(artifactVersion);
+ }
+
+ static ArtifactVersionDependencyVersion parse(String version) {
+ ArtifactVersion artifactVersion = new DefaultArtifactVersion(version);
+ if (artifactVersion.getQualifier() != null && artifactVersion.getQualifier().equals(version)) {
+ return null;
+ }
+ return new ArtifactVersionDependencyVersion(artifactVersion);
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/CombinedPatchAndQualifierDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/CombinedPatchAndQualifierDependencyVersion.java
new file mode 100644
index 0000000000..8a910d6b60
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/CombinedPatchAndQualifierDependencyVersion.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
+
+/**
+ * A {@link DependencyVersion} where the patch and qualifier are not separated.
+ *
+ * @author Andy Wilkinson
+ */
+final class CombinedPatchAndQualifierDependencyVersion extends ArtifactVersionDependencyVersion {
+
+ private static final Pattern PATTERN = Pattern.compile("([0-9]+\\.[0-9]+\\.[0-9]+)([A-Za-z][A-Za-z0-9]+)");
+
+ private final String original;
+
+ private CombinedPatchAndQualifierDependencyVersion(ArtifactVersion artifactVersion, String original) {
+ super(artifactVersion);
+ this.original = original;
+ }
+
+ @Override
+ public String toString() {
+ return this.original;
+ }
+
+ static CombinedPatchAndQualifierDependencyVersion parse(String version) {
+ Matcher matcher = PATTERN.matcher(version);
+ if (!matcher.matches()) {
+ return null;
+ }
+ ArtifactVersion artifactVersion = new DefaultArtifactVersion(matcher.group(1) + "." + matcher.group(2));
+ if (artifactVersion.getQualifier() != null && artifactVersion.getQualifier().equals(version)) {
+ return null;
+ }
+ return new CombinedPatchAndQualifierDependencyVersion(artifactVersion, version);
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/DependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/DependencyVersion.java
new file mode 100644
index 0000000000..a344a5c0e6
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/DependencyVersion.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Version of a dependency.
+ *
+ * @author Andy Wilkinson
+ */
+public interface DependencyVersion extends Comparable {
+
+ /**
+ * Returns whether this version is newer than the given {@code other} version.
+ * @param other version to test
+ * @return {@code true} if this version is newer, otherwise {@code false}
+ */
+ boolean isNewerThan(DependencyVersion other);
+
+ /**
+ * Returns whether this version has the same major versions as the {@code other}
+ * version while also being newer.
+ * @param other version to test
+ * @return {@code true} if this version has the same major and is newer, otherwise
+ * {@code false}
+ */
+ boolean isSameMajorAndNewerThan(DependencyVersion other);
+
+ /**
+ * Returns whether this version has the same major and minor versions as the
+ * {@code other} version while also being newer.
+ * @param other version to test
+ * @return {@code true} if this version has the same major and minor and is newer,
+ * otherwise {@code false}
+ */
+ boolean isSameMinorAndNewerThan(DependencyVersion other);
+
+ static DependencyVersion parse(String version) {
+ List> parsers = Arrays.asList(ArtifactVersionDependencyVersion::parse,
+ ReleaseTrainDependencyVersion::parse, NumericQualifierDependencyVersion::parse,
+ CombinedPatchAndQualifierDependencyVersion::parse, LeadingZeroesDependencyVersion::parse,
+ UnstructuredDependencyVersion::parse);
+ for (Function parser : parsers) {
+ DependencyVersion result = parser.apply(version);
+ if (result != null) {
+ return result;
+ }
+ }
+ throw new IllegalArgumentException("Version '" + version + "' could not be parsed");
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/LeadingZeroesDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/LeadingZeroesDependencyVersion.java
new file mode 100644
index 0000000000..5514b8b652
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/LeadingZeroesDependencyVersion.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
+
+/**
+ * A {@link DependencyVersion} that tolerates leading zeroes.
+ *
+ * @author Andy Wilkinson
+ */
+final class LeadingZeroesDependencyVersion extends ArtifactVersionDependencyVersion {
+
+ private static final Pattern PATTERN = Pattern.compile("0*([0-9]+)\\.0*([0-9]+)\\.0*([0-9]+)");
+
+ private final String original;
+
+ private LeadingZeroesDependencyVersion(ArtifactVersion artifactVersion, String original) {
+ super(artifactVersion);
+ this.original = original;
+ }
+
+ @Override
+ public String toString() {
+ return this.original;
+ }
+
+ static LeadingZeroesDependencyVersion parse(String input) {
+ Matcher matcher = PATTERN.matcher(input);
+ if (!matcher.matches()) {
+ return null;
+ }
+ ArtifactVersion artifactVersion = new DefaultArtifactVersion(
+ matcher.group(1) + matcher.group(2) + matcher.group(3));
+ return new LeadingZeroesDependencyVersion(artifactVersion, input);
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/NumericQualifierDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/NumericQualifierDependencyVersion.java
new file mode 100644
index 0000000000..363def7d80
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/NumericQualifierDependencyVersion.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import org.apache.maven.artifact.versioning.ArtifactVersion;
+import org.apache.maven.artifact.versioning.ComparableVersion;
+import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
+
+/**
+ * A fallback {@link DependencyVersion} to handle versions with four components that
+ * cannot be handled by {@link ArtifactVersion} because the fourth component is numeric.
+ *
+ * @author Andy Wilkinson
+ */
+final class NumericQualifierDependencyVersion extends ArtifactVersionDependencyVersion {
+
+ private final String original;
+
+ private NumericQualifierDependencyVersion(ArtifactVersion artifactVersion, String original) {
+ super(artifactVersion, new ComparableVersion(original));
+ this.original = original;
+ }
+
+ @Override
+ public String toString() {
+ return this.original;
+ }
+
+ static NumericQualifierDependencyVersion parse(String input) {
+ String[] components = input.split("\\.");
+ if (components.length == 4) {
+ ArtifactVersion artifactVersion = new DefaultArtifactVersion(
+ components[0] + "." + components[1] + "." + components[2]);
+ if (artifactVersion.getQualifier() != null && artifactVersion.getQualifier().equals(input)) {
+ return null;
+ }
+ return new NumericQualifierDependencyVersion(artifactVersion, input);
+ }
+ return null;
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java
new file mode 100644
index 0000000000..b3626d1984
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersion.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * A {@link DependencyVersion} for a release train such as Spring Data.
+ *
+ * @author Andy Wilkinson
+ */
+final class ReleaseTrainDependencyVersion implements DependencyVersion {
+
+ private static final Pattern VERSION_PATTERN = Pattern.compile("([A-Z][a-z]+)-([A-Z]+)([0-9]*)");
+
+ private final String releaseTrain;
+
+ private final String type;
+
+ private final int version;
+
+ private final String original;
+
+ private ReleaseTrainDependencyVersion(String releaseTrain, String type, int version, String original) {
+ this.releaseTrain = releaseTrain;
+ this.type = type;
+ this.version = version;
+ this.original = original;
+ }
+
+ @Override
+ public int compareTo(DependencyVersion other) {
+ if (!(other instanceof ReleaseTrainDependencyVersion)) {
+ return 0;
+ }
+ ReleaseTrainDependencyVersion otherReleaseTrain = (ReleaseTrainDependencyVersion) other;
+ int comparison = this.releaseTrain.compareTo(otherReleaseTrain.releaseTrain);
+ if (comparison != 0) {
+ return comparison;
+ }
+ comparison = this.type.compareTo(otherReleaseTrain.type);
+ if (comparison != 0) {
+ return comparison;
+ }
+ return Integer.compare(this.version, otherReleaseTrain.version);
+ }
+
+ @Override
+ public boolean isNewerThan(DependencyVersion other) {
+ if (!(other instanceof ReleaseTrainDependencyVersion)) {
+ return true;
+ }
+ ReleaseTrainDependencyVersion otherReleaseTrain = (ReleaseTrainDependencyVersion) other;
+ return otherReleaseTrain.compareTo(this) < 0;
+ }
+
+ @Override
+ public boolean isSameMajorAndNewerThan(DependencyVersion other) {
+ return isNewerThan(other);
+ }
+
+ @Override
+ public boolean isSameMinorAndNewerThan(DependencyVersion other) {
+ if (!(other instanceof ReleaseTrainDependencyVersion)) {
+ return true;
+ }
+ ReleaseTrainDependencyVersion otherReleaseTrain = (ReleaseTrainDependencyVersion) other;
+ return otherReleaseTrain.releaseTrain.equals(this.releaseTrain) && isNewerThan(other);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ReleaseTrainDependencyVersion other = (ReleaseTrainDependencyVersion) obj;
+ if (!this.original.equals(other.original)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return this.original.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return this.original;
+ }
+
+ static ReleaseTrainDependencyVersion parse(String input) {
+ Matcher matcher = VERSION_PATTERN.matcher(input);
+ if (!matcher.matches()) {
+ return null;
+ }
+ return new ReleaseTrainDependencyVersion(matcher.group(1), matcher.group(2),
+ (StringUtils.hasLength(matcher.group(3))) ? Integer.parseInt(matcher.group(3)) : 0, input);
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/UnstructuredDependencyVersion.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/UnstructuredDependencyVersion.java
new file mode 100644
index 0000000000..02822beaec
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/bomr/version/UnstructuredDependencyVersion.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import org.apache.maven.artifact.versioning.ComparableVersion;
+
+/**
+ * A {@link DependencyVersion} with no structure such that version comparisons are not
+ * possible.
+ *
+ * @author Andy Wilkinson
+ */
+final class UnstructuredDependencyVersion extends AbstractDependencyVersion implements DependencyVersion {
+
+ private final String version;
+
+ private UnstructuredDependencyVersion(String version) {
+ super(new ComparableVersion(version));
+ this.version = version;
+ }
+
+ @Override
+ public boolean isNewerThan(DependencyVersion other) {
+ return this.compareTo(other) > 0;
+ }
+
+ @Override
+ public boolean isSameMajorAndNewerThan(DependencyVersion other) {
+ return this.compareTo(other) > 0;
+ }
+
+ @Override
+ public boolean isSameMinorAndNewerThan(DependencyVersion other) {
+ return this.compareTo(other) > 0;
+ }
+
+ @Override
+ public String toString() {
+ return this.version;
+ }
+
+ static UnstructuredDependencyVersion parse(String version) {
+ return new UnstructuredDependencyVersion(version);
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForConflicts.java b/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForConflicts.java
new file mode 100644
index 0000000000..b5096ed115
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForConflicts.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2012-2020 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.classpath;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.function.Predicate;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.stream.Collectors;
+
+import org.gradle.api.DefaultTask;
+import org.gradle.api.GradleException;
+import org.gradle.api.Task;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.tasks.Classpath;
+import org.gradle.api.tasks.TaskAction;
+
+/**
+ * A {@link Task} for checking the classpath for conflicting classes and resources.
+ *
+ * @author Andy Wilkinson
+ */
+public class CheckClasspathForConflicts extends DefaultTask {
+
+ private final List> ignores = new ArrayList<>();
+
+ private FileCollection classpath;
+
+ public void setClasspath(FileCollection classpath) {
+ this.classpath = classpath;
+ }
+
+ @Classpath
+ public FileCollection getClasspath() {
+ return this.classpath;
+ }
+
+ @TaskAction
+ public void checkForConflicts() throws IOException {
+ ClasspathContents classpathContents = new ClasspathContents();
+ for (File file : this.classpath) {
+ if (file.isDirectory()) {
+ Path root = file.toPath();
+ Files.walk(root).filter((path) -> Files.isRegularFile(path))
+ .forEach((entry) -> classpathContents.add(root.relativize(entry).toString(), root.toString()));
+ }
+ else {
+ try (JarFile jar = new JarFile(file)) {
+ for (JarEntry entry : Collections.list(jar.entries())) {
+ if (!entry.isDirectory()) {
+ classpathContents.add(entry.getName(), file.getAbsolutePath());
+ }
+ }
+ }
+ }
+ }
+ Map> conflicts = classpathContents.getConflicts(this.ignores);
+ if (!conflicts.isEmpty()) {
+ StringBuilder message = new StringBuilder(String.format("Found classpath conflicts:%n"));
+ conflicts.forEach((entry, locations) -> {
+ message.append(String.format(" %s%n", entry));
+ locations.forEach((location) -> message.append(String.format(" %s%n", location)));
+ });
+ throw new GradleException(message.toString());
+ }
+ }
+
+ public void ignore(Predicate predicate) {
+ this.ignores.add(predicate);
+ }
+
+ private static final class ClasspathContents {
+
+ private static final Set IGNORED_NAMES = new HashSet<>(Arrays.asList("about.html", "changelog.txt",
+ "LICENSE", "license.txt", "module-info.class", "notice.txt", "readme.txt"));
+
+ private final Map> classpathContents = new HashMap<>();
+
+ private void add(String name, String source) {
+ this.classpathContents.computeIfAbsent(name, (key) -> new ArrayList<>()).add(source);
+ }
+
+ private Map> getConflicts(List> ignores) {
+ return this.classpathContents.entrySet().stream().filter((entry) -> entry.getValue().size() > 1)
+ .filter((entry) -> canConflict(entry.getKey(), ignores))
+ .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v1, TreeMap::new));
+ }
+
+ private boolean canConflict(String name, List> ignores) {
+ if (name.startsWith("META-INF/")) {
+ return false;
+ }
+ for (String ignoredName : IGNORED_NAMES) {
+ if (name.equals(ignoredName)) {
+ return false;
+ }
+ }
+ for (Predicate ignore : ignores) {
+ if (ignore.test(name)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java b/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java
new file mode 100644
index 0000000000..301304deb7
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/classpath/CheckClasspathForProhibitedDependencies.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2012-2020 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.classpath;
+
+import java.io.IOException;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+
+import org.gradle.api.DefaultTask;
+import org.gradle.api.GradleException;
+import org.gradle.api.Task;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.ModuleVersionIdentifier;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.tasks.Classpath;
+import org.gradle.api.tasks.TaskAction;
+
+/**
+ * A {@link Task} for checking the classpath for prohibited dependencies.
+ *
+ * @author Andy Wilkinson
+ */
+public class CheckClasspathForProhibitedDependencies extends DefaultTask {
+
+ private Configuration classpath;
+
+ public CheckClasspathForProhibitedDependencies() {
+ getOutputs().upToDateWhen((task) -> true);
+ }
+
+ public void setClasspath(Configuration classpath) {
+ this.classpath = classpath;
+ }
+
+ @Classpath
+ public FileCollection getClasspath() {
+ return this.classpath;
+ }
+
+ @TaskAction
+ public void checkForProhibitedDependencies() throws IOException {
+ TreeSet prohibited = this.classpath.getResolvedConfiguration().getResolvedArtifacts().stream()
+ .map((artifact) -> artifact.getModuleVersion().getId()).filter(this::prohibited)
+ .map((id) -> id.getGroup() + ":" + id.getName()).collect(Collectors.toCollection(TreeSet::new));
+ if (!prohibited.isEmpty()) {
+ StringBuilder message = new StringBuilder(String.format("Found prohibited dependencies:%n"));
+ for (String dependency : prohibited) {
+ message.append(String.format(" %s%n", dependency));
+ }
+ throw new GradleException(message.toString());
+ }
+ }
+
+ private boolean prohibited(ModuleVersionIdentifier id) {
+ String group = id.getGroup();
+ if (group.equals("javax.batch")) {
+ return false;
+ }
+ if (group.startsWith("javax")) {
+ return true;
+ }
+ if (group.equals("commons-logging")) {
+ return true;
+ }
+ if (group.equals("org.slf4j") && id.getName().equals("jcl-over-slf4j")) {
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/cli/AbstractPackageManagerDefinitionTask.java b/buildSrc/src/main/java/org/springframework/boot/build/cli/AbstractPackageManagerDefinitionTask.java
new file mode 100644
index 0000000000..f0592979d3
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/cli/AbstractPackageManagerDefinitionTask.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2019 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.cli;
+
+import java.io.File;
+import java.security.MessageDigest;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.Project;
+import org.gradle.api.file.RegularFile;
+import org.gradle.api.provider.Provider;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.TaskExecutionException;
+
+/**
+ * Base class for generating a package manager definition file such as a Scoop manifest or
+ * a Homebrew formula.
+ *
+ * @author Andy Wilkinson
+ */
+public abstract class AbstractPackageManagerDefinitionTask extends DefaultTask {
+
+ private Provider archive;
+
+ private File template;
+
+ private File outputDir;
+
+ public AbstractPackageManagerDefinitionTask() {
+ getInputs().property("version", getProject().provider(getProject()::getVersion));
+ }
+
+ @InputFile
+ public RegularFile getArchive() {
+ return this.archive.get();
+ }
+
+ public void setArchive(Provider archive) {
+ this.archive = archive;
+ }
+
+ @InputFile
+ public File getTemplate() {
+ return this.template;
+ }
+
+ public void setTemplate(File template) {
+ this.template = template;
+ }
+
+ @OutputDirectory
+ public File getOutputDir() {
+ return this.outputDir;
+ }
+
+ public void setOutputDir(File outputDir) {
+ this.outputDir = outputDir;
+ }
+
+ protected void createDescriptor(Map additionalProperties) {
+ getProject().copy((copy) -> {
+ copy.from(this.template);
+ copy.into(this.outputDir);
+ Map properties = new HashMap<>(additionalProperties);
+ properties.put("hash", sha256(this.archive.get().getAsFile()));
+ properties.put("repo", determineArtifactoryRepo(getProject()));
+ properties.put("project", getProject());
+ copy.expand(properties);
+ });
+ }
+
+ private String sha256(File file) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ return new DigestUtils(digest).digestAsHex(file);
+ }
+ catch (Exception ex) {
+ throw new TaskExecutionException(this, ex);
+ }
+ }
+
+ private String determineArtifactoryRepo(Project project) {
+ String version = project.getVersion().toString();
+ String type = version.substring(version.lastIndexOf('.'));
+ if (type.equals("RELEASE")) {
+ return "release";
+ }
+ if (type.startsWith("M") || type.startsWith("RC")) {
+ return "milestone";
+ }
+ return "snapshot";
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/cli/HomebrewFormula.java b/buildSrc/src/main/java/org/springframework/boot/build/cli/HomebrewFormula.java
new file mode 100644
index 0000000000..5592d30617
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/cli/HomebrewFormula.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2012-2020 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.cli;
+
+import java.util.Collections;
+
+import org.gradle.api.tasks.TaskAction;
+
+/**
+ * A {@Task} for creating a Homebrew formula manifest.
+ *
+ * @author Andy Wilkinson
+ */
+public class HomebrewFormula extends AbstractPackageManagerDefinitionTask {
+
+ @TaskAction
+ void createFormula() {
+ createDescriptor(Collections.emptyMap());
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/cli/ScoopManifest.java b/buildSrc/src/main/java/org/springframework/boot/build/cli/ScoopManifest.java
new file mode 100644
index 0000000000..de671c8da1
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/cli/ScoopManifest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2012-2020 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.cli;
+
+import java.util.Collections;
+
+import org.gradle.api.tasks.TaskAction;
+
+/**
+ * A {@Task} for creating a Scoop manifest.
+ *
+ * @author Andy Wilkinson
+ */
+public class ScoopManifest extends AbstractPackageManagerDefinitionTask {
+
+ @TaskAction
+ void createManifest() {
+ String version = getProject().getVersion().toString();
+ createDescriptor(Collections.singletonMap("scoopVersion", version.substring(0, version.lastIndexOf('.'))));
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/constraints/DocumentConstrainedVersions.java b/buildSrc/src/main/java/org/springframework/boot/build/constraints/DocumentConstrainedVersions.java
new file mode 100644
index 0000000000..101039e9fd
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/constraints/DocumentConstrainedVersions.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2019 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.constraints;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import javax.inject.Inject;
+
+import org.gradle.api.DefaultTask;
+import org.gradle.api.model.ObjectFactory;
+import org.gradle.api.provider.SetProperty;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+
+import org.springframework.boot.build.constraints.ExtractVersionConstraints.ConstrainedVersion;
+
+/**
+ * Task for documenting a platform's constrained versions.
+ *
+ * @author Andy Wilkinson
+ */
+public class DocumentConstrainedVersions extends DefaultTask {
+
+ private final SetProperty constrainedVersions;
+
+ private File outputFile;
+
+ @Inject
+ public DocumentConstrainedVersions(ObjectFactory objectFactory) {
+ this.constrainedVersions = objectFactory.setProperty(ConstrainedVersion.class);
+ }
+
+ @Input
+ public SetProperty getConstrainedVersions() {
+ return this.constrainedVersions;
+ }
+
+ @OutputFile
+ public File getOutputFile() {
+ return this.outputFile;
+ }
+
+ public void setOutputFile(File outputFile) {
+ this.outputFile = outputFile;
+ }
+
+ @TaskAction
+ public void documentConstrainedVersions() throws IOException {
+ this.outputFile.getParentFile().mkdirs();
+ try (PrintWriter writer = new PrintWriter(new FileWriter(this.outputFile))) {
+ writer.println("|===");
+ writer.println("| Group ID | Artifact ID | Version");
+ for (ConstrainedVersion constrainedVersion : this.constrainedVersions.get()) {
+ writer.println();
+ writer.printf("| `%s`%n", constrainedVersion.getGroup());
+ writer.printf("| `%s`%n", constrainedVersion.getArtifact());
+ writer.printf("| `%s`%n", constrainedVersion.getVersion());
+ }
+ writer.println("|===");
+ }
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/constraints/ExtractVersionConstraints.java b/buildSrc/src/main/java/org/springframework/boot/build/constraints/ExtractVersionConstraints.java
new file mode 100644
index 0000000000..3151560b7f
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/constraints/ExtractVersionConstraints.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2019 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.constraints;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.gradle.api.Task;
+import org.gradle.api.artifacts.ComponentMetadataDetails;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.DependencyConstraint;
+import org.gradle.api.artifacts.DependencyConstraintMetadata;
+import org.gradle.api.artifacts.dsl.DependencyHandler;
+import org.gradle.api.internal.AbstractTask;
+import org.gradle.api.tasks.Internal;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.platform.base.Platform;
+
+/**
+ * {@link Task} to extract constraints from a {@link Platform}. The platform's own
+ * constraints and those in any boms upon which it depends are extracted.
+ *
+ * @author Andy Wilkinson
+ */
+public class ExtractVersionConstraints extends AbstractTask {
+
+ private final Configuration configuration;
+
+ private final Map versionConstraints = new TreeMap<>();
+
+ private final Set constrainedVersions = new TreeSet<>();
+
+ private final List projectPaths = new ArrayList();
+
+ public ExtractVersionConstraints() {
+ DependencyHandler dependencies = getProject().getDependencies();
+ this.configuration = getProject().getConfigurations().create(getName());
+ dependencies.getComponents().all(this::processMetadataDetails);
+ }
+
+ public void enforcedPlatform(String projectPath) {
+ this.configuration.getDependencies().add(getProject().getDependencies().enforcedPlatform(
+ getProject().getDependencies().project(Collections.singletonMap("path", projectPath))));
+ this.projectPaths.add(projectPath);
+ }
+
+ @Internal
+ public Map getVersionConstraints() {
+ return Collections.unmodifiableMap(this.versionConstraints);
+ }
+
+ @Internal
+ public Set getConstrainedVersions() {
+ return this.constrainedVersions;
+ }
+
+ @TaskAction
+ void extractVersionConstraints() {
+ this.configuration.resolve();
+ for (String projectPath : this.projectPaths) {
+ for (DependencyConstraint constraint : getProject().project(projectPath).getConfigurations()
+ .getByName("apiElements").getAllDependencyConstraints()) {
+ this.versionConstraints.put(constraint.getGroup() + ":" + constraint.getName(),
+ constraint.getVersionConstraint().toString());
+ this.constrainedVersions.add(new ConstrainedVersion(constraint.getGroup(), constraint.getName(),
+ constraint.getVersionConstraint().toString()));
+ }
+ }
+ }
+
+ private void processMetadataDetails(ComponentMetadataDetails details) {
+ details.allVariants((variantMetadata) -> variantMetadata.withDependencyConstraints((dependencyConstraints) -> {
+ for (DependencyConstraintMetadata constraint : dependencyConstraints) {
+ this.versionConstraints.put(constraint.getGroup() + ":" + constraint.getName(),
+ constraint.getVersionConstraint().toString());
+ this.constrainedVersions.add(new ConstrainedVersion(constraint.getGroup(), constraint.getName(),
+ constraint.getVersionConstraint().toString()));
+ }
+ }));
+ }
+
+ public static final class ConstrainedVersion implements Comparable, Serializable {
+
+ private final String group;
+
+ private final String artifact;
+
+ private final String version;
+
+ private ConstrainedVersion(String group, String artifact, String version) {
+ this.group = group;
+ this.artifact = artifact;
+ this.version = version;
+ }
+
+ public String getGroup() {
+ return this.group;
+ }
+
+ public String getArtifact() {
+ return this.artifact;
+ }
+
+ public String getVersion() {
+ return this.version;
+ }
+
+ @Override
+ public int compareTo(ConstrainedVersion other) {
+ int groupComparison = this.group.compareTo(other.group);
+ if (groupComparison != 0) {
+ return groupComparison;
+ }
+ return this.artifact.compareTo(other.artifact);
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/AsciidocBuilder.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/AsciidocBuilder.java
new file mode 100644
index 0000000000..305da3abcf
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/AsciidocBuilder.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+/**
+ * Simple builder to help construct Asciidoc markup.
+ *
+ * @author Phillip Webb
+ */
+class AsciidocBuilder {
+
+ private final StringBuilder content;
+
+ AsciidocBuilder() {
+ this.content = new StringBuilder();
+ }
+
+ AsciidocBuilder appendKey(Object... items) {
+ for (Object item : items) {
+ appendln("`+", item, "+` +");
+ }
+ return this;
+ }
+
+ AsciidocBuilder newLine() {
+ return append(System.lineSeparator());
+ }
+
+ AsciidocBuilder appendln(Object... items) {
+ return append(items).newLine();
+ }
+
+ AsciidocBuilder append(Object... items) {
+ for (Object item : items) {
+ this.content.append(item);
+ }
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return this.content.toString();
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CompoundConfigurationTableEntry.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CompoundConfigurationTableEntry.java
new file mode 100644
index 0000000000..f045b5d730
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/CompoundConfigurationTableEntry.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Stream;
+
+/**
+ * Table entry regrouping a list of configuration properties sharing the same description.
+ *
+ * @author Brian Clozel
+ */
+class CompoundConfigurationTableEntry extends ConfigurationTableEntry {
+
+ private Set configurationKeys;
+
+ private String description;
+
+ CompoundConfigurationTableEntry(String key, String description) {
+ this.key = key;
+ this.description = description;
+ this.configurationKeys = new TreeSet<>();
+ }
+
+ void addConfigurationKeys(ConfigurationProperty... properties) {
+ Stream.of(properties).map(ConfigurationProperty::getName).forEach(this.configurationKeys::add);
+ }
+
+ @Override
+ void write(AsciidocBuilder builder) {
+ builder.append("|");
+ this.configurationKeys.forEach(builder::appendKey);
+ builder.newLine().appendln("|").appendln("|+++", this.description, "+++");
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationMetadataDocumentWriter.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationMetadataDocumentWriter.java
new file mode 100644
index 0000000000..f566107322
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationMetadataDocumentWriter.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.gradle.api.file.FileCollection;
+
+/**
+ * Write Asciidoc documents with configuration properties listings.
+ *
+ * @author Brian Clozel
+ * @since 2.0.0
+ */
+public class ConfigurationMetadataDocumentWriter {
+
+ public void writeDocument(Path outputDirectory, DocumentOptions options, FileCollection metadataFiles)
+ throws IOException {
+ assertValidOutputDirectory(outputDirectory);
+ if (!Files.exists(outputDirectory)) {
+ Files.createDirectory(outputDirectory);
+ }
+ List tables = createConfigTables(ConfigurationProperties.fromFiles(metadataFiles), options);
+ for (ConfigurationTable table : tables) {
+ writeConfigurationTable(table, outputDirectory);
+ }
+ }
+
+ private void assertValidOutputDirectory(Path outputDirPath) {
+ if (outputDirPath == null) {
+ throw new IllegalArgumentException("output path should not be null");
+ }
+ if (Files.exists(outputDirPath) && !Files.isDirectory(outputDirPath)) {
+ throw new IllegalArgumentException("output path already exists and is not a directory");
+ }
+ }
+
+ private List createConfigTables(Map metadataProperties,
+ DocumentOptions options) {
+ List tables = new ArrayList<>();
+ List unmappedKeys = metadataProperties.values().stream().filter((property) -> !property.isDeprecated())
+ .map(ConfigurationProperty::getName).collect(Collectors.toList());
+ Map overrides = getOverrides(metadataProperties, unmappedKeys,
+ options);
+ options.getMetadataSections().forEach((id, keyPrefixes) -> tables
+ .add(createConfigTable(metadataProperties, unmappedKeys, overrides, id, keyPrefixes)));
+ if (!unmappedKeys.isEmpty()) {
+ throw new IllegalStateException(
+ "The following keys were not written to the documentation: " + String.join(", ", unmappedKeys));
+ }
+ if (!overrides.isEmpty()) {
+ throw new IllegalStateException("The following keys were not written to the documentation: "
+ + String.join(", ", overrides.keySet()));
+ }
+ return tables;
+ }
+
+ private Map getOverrides(
+ Map metadataProperties, List unmappedKeys, DocumentOptions options) {
+ Map overrides = new HashMap<>();
+ options.getOverrides().forEach((keyPrefix, description) -> {
+ CompoundConfigurationTableEntry entry = new CompoundConfigurationTableEntry(keyPrefix, description);
+ List matchingKeys = unmappedKeys.stream().filter((key) -> key.startsWith(keyPrefix))
+ .collect(Collectors.toList());
+ for (String matchingKey : matchingKeys) {
+ entry.addConfigurationKeys(metadataProperties.get(matchingKey));
+ }
+ overrides.put(keyPrefix, entry);
+ unmappedKeys.removeAll(matchingKeys);
+ });
+ return overrides;
+ }
+
+ private ConfigurationTable createConfigTable(Map metadataProperties,
+ List unmappedKeys, Map overrides, String id,
+ List keyPrefixes) {
+ ConfigurationTable table = new ConfigurationTable(id);
+ for (String keyPrefix : keyPrefixes) {
+ List matchingOverrides = overrides.keySet().stream()
+ .filter((overrideKey) -> overrideKey.startsWith(keyPrefix)).collect(Collectors.toList());
+ matchingOverrides.forEach((match) -> table.addEntry(overrides.remove(match)));
+ }
+ List matchingKeys = unmappedKeys.stream()
+ .filter((key) -> keyPrefixes.stream().anyMatch(key::startsWith)).collect(Collectors.toList());
+ for (String matchingKey : matchingKeys) {
+ ConfigurationProperty property = metadataProperties.get(matchingKey);
+ table.addEntry(new SingleConfigurationTableEntry(property));
+ }
+ unmappedKeys.removeAll(matchingKeys);
+ return table;
+ }
+
+ private void writeConfigurationTable(ConfigurationTable table, Path outputDirectory) throws IOException {
+ Path outputFilePath = outputDirectory.resolve(table.getId() + ".adoc");
+ Files.deleteIfExists(outputFilePath);
+ Files.createFile(outputFilePath);
+ try (OutputStream outputStream = Files.newOutputStream(outputFilePath)) {
+ outputStream.write(table.toAsciidocTable().getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationProperties.java
new file mode 100644
index 0000000000..5fa43adee1
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationProperties.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import org.gradle.api.file.FileCollection;
+
+/**
+ * Configuration properties read from one or more
+ * {@code META-INF/spring-configuration-metadata.json} files.
+ *
+ * @author Andy Wilkinson
+ */
+final class ConfigurationProperties {
+
+ private static final Type MAP_TYPE = new MapTypeToken().getType();
+
+ private ConfigurationProperties() {
+
+ }
+
+ @SuppressWarnings("unchecked")
+ static Map fromFiles(FileCollection files) {
+ List configurationProperties = new ArrayList<>();
+ try {
+ Gson gson = new GsonBuilder().create();
+ for (File file : files) {
+ try (Reader reader = new FileReader(file)) {
+ Map json = gson.fromJson(reader, MAP_TYPE);
+ List> properties = (List>) json.get("properties");
+ for (Map property : properties) {
+ String name = (String) property.get("name");
+ String type = (String) property.get("type");
+ Object defaultValue = property.get("defaultValue");
+ String description = (String) property.get("description");
+ boolean deprecated = property.containsKey("deprecated");
+ configurationProperties
+ .add(new ConfigurationProperty(name, type, defaultValue, description, deprecated));
+ }
+ }
+ }
+ return configurationProperties.stream()
+ .collect(Collectors.toMap(ConfigurationProperty::getName, Function.identity()));
+ }
+ catch (IOException ex) {
+ throw new RuntimeException("Failed to load configuration metadata", ex);
+ }
+ }
+
+ private static final class MapTypeToken extends TypeToken> {
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesPlugin.java
new file mode 100644
index 0000000000..81f51281fb
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationPropertiesPlugin.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.plugins.JavaPluginConvention;
+import org.gradle.api.tasks.SourceSet;
+import org.gradle.api.tasks.compile.JavaCompile;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link Plugin} for projects that define {@code @ConfigurationProperties}. When applied,
+ * the plugin reacts to the presence of the {@link JavaPlugin} by:
+ *
+ *
+ * Adding a dependency on the configuration properties annotation processor.
+ * Configure the additional metadata locations annotation processor compiler argument
+ * Defining an artifact for the resulting configuration property metadata so that it
+ * can be consumed by downstream projects.
+ *
+ *
+ * @author Andy Wilkinson
+ */
+public class ConfigurationPropertiesPlugin implements Plugin {
+
+ /**
+ * Name of the {@link Configuration} that holds the configuration property metadata
+ * artifact.
+ */
+ public static final String CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME = "configurationPropertiesMetadata";
+
+ @Override
+ public void apply(Project project) {
+ project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
+ addConfigurationProcessorDependency(project);
+ configureAdditionalMetadataLocationsCompilerArgument(project);
+ addMetadataArtifact(project);
+ });
+ }
+
+ private void addConfigurationProcessorDependency(Project project) {
+ Configuration annotationProcessors = project.getConfigurations()
+ .getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME);
+ annotationProcessors.getDependencies().add(project.getDependencies().project(Collections.singletonMap("path",
+ ":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor")));
+ }
+
+ private void addMetadataArtifact(Project project) {
+ SourceSet mainSourceSet = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets()
+ .getByName(SourceSet.MAIN_SOURCE_SET_NAME);
+ project.getConfigurations().maybeCreate(CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME);
+ project.afterEvaluate((evaluatedProject) -> evaluatedProject.getArtifacts().add(
+ CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME,
+ evaluatedProject.provider((Callable) () -> new File(mainSourceSet.getJava().getOutputDir(),
+ "META-INF/spring-configuration-metadata.json")),
+ (artifact) -> artifact
+ .builtBy(evaluatedProject.getTasks().getByName(mainSourceSet.getClassesTaskName()))));
+ }
+
+ private void configureAdditionalMetadataLocationsCompilerArgument(Project project) {
+ JavaCompile compileJava = project.getTasks().withType(JavaCompile.class)
+ .getByName(JavaPlugin.COMPILE_JAVA_TASK_NAME);
+ SourceSet mainSourceSet = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets()
+ .getByName(SourceSet.MAIN_SOURCE_SET_NAME);
+ compileJava.getOptions().getCompilerArgs()
+ .add("-Aorg.springframework.boot.configurationprocessor.additionalMetadataLocations=" + StringUtils
+ .collectionToCommaDelimitedString(mainSourceSet.getResources().getSourceDirectories().getFiles()
+ .stream().map(project.getRootProject()::relativePath).collect(Collectors.toSet())));
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationProperty.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationProperty.java
new file mode 100644
index 0000000000..e66c236f47
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationProperty.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2019 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.context.properties;
+
+/**
+ * A configuration property.
+ *
+ * @author Andy Wilkinson
+ */
+public class ConfigurationProperty {
+
+ private final String name;
+
+ private final String type;
+
+ private final Object defaultValue;
+
+ private final String description;
+
+ private final boolean deprecated;
+
+ ConfigurationProperty(String name, String type) {
+ this(name, type, null, null, false);
+ }
+
+ ConfigurationProperty(String name, String type, Object defaultValue, String description, boolean deprecated) {
+ this.name = name;
+ this.type = type;
+ this.defaultValue = defaultValue;
+ this.description = description;
+ this.deprecated = deprecated;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public Object getDefaultValue() {
+ return this.defaultValue;
+ }
+
+ public String getDescription() {
+ return this.description;
+ }
+
+ public boolean isDeprecated() {
+ return this.deprecated;
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationTable.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationTable.java
new file mode 100644
index 0000000000..533c0b8365
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationTable.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Asciidoctor table listing configuration properties sharing to a common theme.
+ *
+ * @author Brian Clozel
+ */
+class ConfigurationTable {
+
+ private final String id;
+
+ private final Set entries;
+
+ ConfigurationTable(String id) {
+ this.id = id;
+ this.entries = new TreeSet<>();
+ }
+
+ String getId() {
+ return this.id;
+ }
+
+ void addEntry(ConfigurationTableEntry... entries) {
+ this.entries.addAll(Arrays.asList(entries));
+ }
+
+ String toAsciidocTable() {
+ AsciidocBuilder builder = new AsciidocBuilder();
+ builder.appendln("[cols=\"1,1,2\", options=\"header\"]");
+ builder.appendln("|===");
+ builder.appendln("|Key|Default Value|Description");
+ builder.appendln();
+ this.entries.forEach((entry) -> {
+ entry.write(builder);
+ builder.appendln();
+ });
+ return builder.appendln("|===").toString();
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationTableEntry.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationTableEntry.java
new file mode 100644
index 0000000000..a0ebc3361b
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/ConfigurationTableEntry.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+/**
+ * Abstract class for entries in {@link ConfigurationTable}.
+ *
+ * @author Brian Clozel
+ */
+abstract class ConfigurationTableEntry implements Comparable {
+
+ protected String key;
+
+ String getKey() {
+ return this.key;
+ }
+
+ abstract void write(AsciidocBuilder builder);
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ ConfigurationTableEntry other = (ConfigurationTableEntry) obj;
+ return this.key.equals(other.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.key.hashCode();
+ }
+
+ @Override
+ public int compareTo(ConfigurationTableEntry other) {
+ return this.key.compareTo(other.getKey());
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java
new file mode 100644
index 0000000000..bb38138e2c
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import java.io.File;
+import java.io.IOException;
+
+import org.gradle.api.Task;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.internal.AbstractTask;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.TaskAction;
+
+import org.springframework.boot.build.context.properties.DocumentOptions.Builder;
+
+/**
+ * {@link Task} used to document auto-configuration classes.
+ *
+ * @author Andy Wilkinson
+ */
+public class DocumentConfigurationProperties extends AbstractTask {
+
+ private FileCollection configurationPropertyMetadata;
+
+ private File outputDir;
+
+ @InputFiles
+ public FileCollection getConfigurationPropertyMetadata() {
+ return this.configurationPropertyMetadata;
+ }
+
+ public void setConfigurationPropertyMetadata(FileCollection configurationPropertyMetadata) {
+ this.configurationPropertyMetadata = configurationPropertyMetadata;
+ }
+
+ @OutputDirectory
+ public File getOutputDir() {
+ return this.outputDir;
+ }
+
+ public void setOutputDir(File outputDir) {
+ this.outputDir = outputDir;
+ }
+
+ @TaskAction
+ void documentConfigurationProperties() throws IOException {
+ Builder builder = DocumentOptions.builder();
+ builder.addSection("core")
+ .withKeyPrefixes("debug", "trace", "logging", "spring.aop", "spring.application",
+ "spring.autoconfigure", "spring.banner", "spring.beaninfo", "spring.codec", "spring.config",
+ "spring.info", "spring.jmx", "spring.main", "spring.messages", "spring.pid", "spring.profiles",
+ "spring.quartz", "spring.reactor", "spring.task", "spring.mandatory-file-encoding", "info",
+ "spring.output.ansi.enabled")
+ .addSection("mail").withKeyPrefixes("spring.mail", "spring.sendgrid").addSection("cache")
+ .withKeyPrefixes("spring.cache").addSection("server").withKeyPrefixes("server").addSection("web")
+ .withKeyPrefixes("spring.hateoas", "spring.http", "spring.servlet", "spring.jersey", "spring.mvc",
+ "spring.resources", "spring.webflux")
+ .addSection("json").withKeyPrefixes("spring.jackson", "spring.gson").addSection("rsocket")
+ .withKeyPrefixes("spring.rsocket").addSection("templating")
+ .withKeyPrefixes("spring.freemarker", "spring.groovy", "spring.mustache", "spring.thymeleaf")
+ .addOverride("spring.groovy.template.configuration", "See GroovyMarkupConfigurer")
+ .addSection("security").withKeyPrefixes("spring.security", "spring.ldap", "spring.session")
+ .addSection("data-migration").withKeyPrefixes("spring.flyway", "spring.liquibase").addSection("data")
+ .withKeyPrefixes("spring.couchbase", "spring.elasticsearch", "spring.h2", "spring.influx",
+ "spring.mongodb", "spring.redis", "spring.dao", "spring.data", "spring.datasource",
+ "spring.jooq", "spring.jdbc", "spring.jpa")
+ .addOverride("spring.datasource.dbcp2", "Commons DBCP2 specific settings")
+ .addOverride("spring.datasource.tomcat", "Tomcat datasource specific settings")
+ .addOverride("spring.datasource.hikari", "Hikari specific settings").addSection("transaction")
+ .withKeyPrefixes("spring.jta", "spring.transaction").addSection("integration")
+ .withKeyPrefixes("spring.activemq", "spring.artemis", "spring.batch", "spring.integration",
+ "spring.jms", "spring.kafka", "spring.rabbitmq", "spring.hazelcast", "spring.webservices")
+ .addSection("actuator").withKeyPrefixes("management").addSection("devtools")
+ .withKeyPrefixes("spring.devtools").addSection("testing").withKeyPrefixes("spring.test");
+ DocumentOptions options = builder.build();
+ new ConfigurationMetadataDocumentWriter().writeDocument(this.outputDir.toPath(), options,
+ this.configurationPropertyMetadata);
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentOptions.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentOptions.java
new file mode 100644
index 0000000000..53fc4adec3
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentOptions.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Options for generating documentation for configuration properties.
+ *
+ * @author Brian Clozel
+ * @since 2.0.0
+ */
+public final class DocumentOptions {
+
+ private final Map> metadataSections;
+
+ private final Map overrides;
+
+ private DocumentOptions(Map> metadataSections, Map overrides) {
+ this.metadataSections = metadataSections;
+ this.overrides = overrides;
+ }
+
+ Map> getMetadataSections() {
+ return this.metadataSections;
+ }
+
+ Map getOverrides() {
+ return this.overrides;
+ }
+
+ static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder for DocumentOptions.
+ */
+ public static class Builder {
+
+ Map> metadataSections = new HashMap<>();
+
+ Map overrides = new HashMap<>();
+
+ SectionSpec addSection(String name) {
+ return new SectionSpec(this, name);
+ }
+
+ Builder addOverride(String keyPrefix, String description) {
+ this.overrides.put(keyPrefix, description);
+ return this;
+ }
+
+ DocumentOptions build() {
+ return new DocumentOptions(this.metadataSections, this.overrides);
+ }
+
+ }
+
+ /**
+ * Configuration for a documentation section listing properties for a specific theme.
+ */
+ public static class SectionSpec {
+
+ private final String name;
+
+ private final Builder builder;
+
+ SectionSpec(Builder builder, String name) {
+ this.builder = builder;
+ this.name = name;
+ }
+
+ Builder withKeyPrefixes(String... keyPrefixes) {
+ this.builder.metadataSections.put(this.name, Arrays.asList(keyPrefixes));
+ return this.builder;
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/SingleConfigurationTableEntry.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/SingleConfigurationTableEntry.java
new file mode 100644
index 0000000000..a269281843
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/SingleConfigurationTableEntry.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+/**
+ * Table entry containing a single configuration property.
+ *
+ * @author Brian Clozel
+ */
+class SingleConfigurationTableEntry extends ConfigurationTableEntry {
+
+ private final String description;
+
+ private final String defaultValue;
+
+ SingleConfigurationTableEntry(ConfigurationProperty property) {
+ this.key = property.getName();
+ if (property.getType() != null && property.getType().startsWith("java.util.Map")) {
+ this.key += ".*";
+ }
+ this.description = property.getDescription();
+ this.defaultValue = getDefaultValue(property.getDefaultValue());
+ }
+
+ private String getDefaultValue(Object defaultValue) {
+ if (defaultValue == null) {
+ return null;
+ }
+ if (defaultValue.getClass().isArray()) {
+ return Arrays.stream((Object[]) defaultValue).map(Object::toString)
+ .collect(Collectors.joining("," + System.lineSeparator()));
+ }
+ return defaultValue.toString();
+ }
+
+ @Override
+ void write(AsciidocBuilder builder) {
+ builder.appendln("|`+", this.key, "+`");
+ writeDefaultValue(builder);
+ writeDescription(builder);
+ builder.appendln();
+ }
+
+ private void writeDefaultValue(AsciidocBuilder builder) {
+ String defaultValue = (this.defaultValue != null) ? this.defaultValue : "";
+ if (defaultValue.isEmpty()) {
+ builder.appendln("|");
+ }
+ else {
+ defaultValue = defaultValue.replace("\\", "\\\\").replace("|", "\\|");
+ builder.appendln("|`+", defaultValue, "+`");
+ }
+ }
+
+ private void writeDescription(AsciidocBuilder builder) {
+ if (this.description == null || this.description.isEmpty()) {
+ builder.append("|");
+ }
+ else {
+ String cleanedDescription = this.description.replace("|", "\\|");
+ builder.append("|+++", cleanedDescription, "+++");
+ }
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/log4j2/ReproducibleLog4j2PluginsDatAction.java b/buildSrc/src/main/java/org/springframework/boot/build/log4j2/ReproducibleLog4j2PluginsDatAction.java
new file mode 100644
index 0000000000..0414fb1cbb
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/log4j2/ReproducibleLog4j2PluginsDatAction.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2012-2020 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.log4j2;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TreeMap;
+
+import org.gradle.api.Action;
+import org.gradle.api.InvalidUserDataException;
+import org.gradle.api.tasks.TaskExecutionException;
+import org.gradle.api.tasks.compile.JavaCompile;
+
+/**
+ * An {@Action} to post-process a {@code Log4j2Plugins.dat} and re-order its content so
+ * that it is reproducible.
+ *
+ * @author Andy Wilkinson
+ */
+public class ReproducibleLog4j2PluginsDatAction implements Action {
+
+ @Override
+ public void execute(JavaCompile javaCompile) {
+ File datFile = new File(javaCompile.getDestinationDir(),
+ "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat");
+ try {
+ postProcess(datFile);
+ }
+ catch (IOException ex) {
+ throw new TaskExecutionException(javaCompile, ex);
+ }
+ }
+
+ void postProcess(File datFile) throws IOException {
+ if (!datFile.isFile()) {
+ throw new InvalidUserDataException(
+ "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat not found");
+ }
+ Map> categories = new TreeMap<>();
+ try (DataInputStream input = new DataInputStream(new FileInputStream(datFile))) {
+ int categoryCount = input.readInt();
+ for (int i = 0; i < categoryCount; i++) {
+ String categoryName = input.readUTF();
+ int pluginCount = input.readInt();
+ Map category = categories.computeIfAbsent(categoryName, (c) -> new TreeMap<>());
+ for (int j = 0; j < pluginCount; j++) {
+ Plugin plugin = new Plugin(input.readUTF(), input.readUTF(), input.readUTF(), input.readBoolean(),
+ input.readBoolean());
+ category.putIfAbsent(plugin.key, plugin);
+ }
+ }
+ }
+ try (DataOutputStream output = new DataOutputStream(new FileOutputStream(datFile))) {
+ output.writeInt(categories.size());
+ for (Entry> category : categories.entrySet()) {
+ output.writeUTF(category.getKey());
+ output.writeInt(category.getValue().size());
+ for (Plugin plugin : category.getValue().values()) {
+ output.writeUTF(plugin.key);
+ output.writeUTF(plugin.className);
+ output.writeUTF(plugin.name);
+ output.writeBoolean(plugin.printable);
+ output.writeBoolean(plugin.defer);
+ }
+ }
+ }
+ }
+
+ private static final class Plugin {
+
+ private final String key;
+
+ private final String className;
+
+ private final String name;
+
+ private final boolean printable;
+
+ private final boolean defer;
+
+ private Plugin(String key, String className, String name, boolean printable, boolean defer) {
+ this.key = key;
+ this.className = className;
+ this.name = name;
+ this.printable = printable;
+ this.defer = defer;
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/DocumentPluginGoals.java b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/DocumentPluginGoals.java
new file mode 100644
index 0000000000..2a16072dbb
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/DocumentPluginGoals.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2012-2020 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.mavenplugin;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.gradle.api.DefaultTask;
+import org.gradle.api.Task;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.TaskAction;
+
+import org.springframework.boot.build.mavenplugin.PluginXmlParser.Mojo;
+import org.springframework.boot.build.mavenplugin.PluginXmlParser.Parameter;
+import org.springframework.boot.build.mavenplugin.PluginXmlParser.Plugin;
+
+/**
+ * A {@link Task} to document the plugin's goals.
+ *
+ * @author Andy Wilkinson
+ */
+public class DocumentPluginGoals extends DefaultTask {
+
+ private final PluginXmlParser parser = new PluginXmlParser();
+
+ private File pluginXml;
+
+ private File outputDir;
+
+ @OutputDirectory
+ public File getOutputDir() {
+ return this.outputDir;
+ }
+
+ public void setOutputDir(File outputDir) {
+ this.outputDir = outputDir;
+ }
+
+ @InputFile
+ public File getPluginXml() {
+ return this.pluginXml;
+ }
+
+ public void setPluginXml(File pluginXml) {
+ this.pluginXml = pluginXml;
+ }
+
+ @TaskAction
+ public void documentPluginGoals() throws IOException {
+ Plugin plugin = this.parser.parse(this.pluginXml);
+ writeOverview(plugin);
+ for (Mojo mojo : plugin.getMojos()) {
+ documentMojo(plugin, mojo);
+ }
+ }
+
+ private void writeOverview(Plugin plugin) throws IOException {
+ try (PrintWriter writer = new PrintWriter(new FileWriter(new File(this.outputDir, "overview.adoc")))) {
+ writer.println("[cols=\"1,3\"]");
+ writer.println("|===");
+ writer.println("| Goal | Description");
+ writer.println();
+ for (Mojo mojo : plugin.getMojos()) {
+ writer.printf("| <>%n", mojo.getGoal(), plugin.getGoalPrefix(), mojo.getGoal());
+ writer.printf("| %s%n", mojo.getDescription());
+ writer.println();
+ }
+ writer.println("|===");
+ }
+ }
+
+ private void documentMojo(Plugin plugin, Mojo mojo) throws IOException {
+ try (PrintWriter writer = new PrintWriter(new FileWriter(new File(this.outputDir, mojo.getGoal() + ".adoc")))) {
+ String sectionId = "goals-" + mojo.getGoal();
+ writer.println();
+ writer.println();
+ writer.printf("[[%s]]%n", sectionId);
+ writer.printf("== `%s:%s`%n", plugin.getGoalPrefix(), mojo.getGoal());
+ writer.printf("`%s:%s:%s`%n", plugin.getGroupId(), plugin.getArtifactId(), plugin.getVersion());
+ writer.println();
+ writer.println(mojo.getDescription());
+ List parameters = mojo.getParameters().stream().filter(Parameter::isEditable)
+ .collect(Collectors.toList());
+ List requiredParameters = parameters.stream().filter(Parameter::isRequired)
+ .collect(Collectors.toList());
+ String parametersSectionId = sectionId + "-parameters";
+ String detailsSectionId = parametersSectionId + "-details";
+ if (!requiredParameters.isEmpty()) {
+ writer.println();
+ writer.println();
+ writer.printf("[[%s-required]]%n", parametersSectionId);
+ writer.println("=== Required parameters");
+ writeParametersTable(writer, mojo.getGoal(), requiredParameters);
+ }
+ List optionalParameters = parameters.stream().filter((parameter) -> !parameter.isRequired())
+ .collect(Collectors.toList());
+ if (!optionalParameters.isEmpty()) {
+ writer.println();
+ writer.println();
+ writer.printf("[[%s-optional]]%n", parametersSectionId);
+ writer.println("=== Optional parameters");
+ writeParametersTable(writer, detailsSectionId, optionalParameters);
+ }
+ writer.println();
+ writer.println();
+ writer.printf("[[%s]]%n", detailsSectionId);
+ writer.println("=== Parameter details");
+ writeParameterDetails(writer, parameters, detailsSectionId);
+ }
+ }
+
+ private void writeParametersTable(PrintWriter writer, String detailsSectionId, List parameters) {
+ writer.println("[cols=\"3,2,3\"]");
+ writer.println("|===");
+ writer.println("| Name | Type | Default");
+ writer.println();
+ for (Parameter parameter : parameters) {
+ String name = parameter.getName();
+ writer.printf("| <<%s-%s,%s>>%n", detailsSectionId, name, name);
+ String type = parameter.getType();
+ if (type.lastIndexOf('.') >= 0) {
+ type = type.substring(type.lastIndexOf('.') + 1);
+ }
+ writer.printf("| `%s`%n", type);
+ String defaultValue = parameter.getDefaultValue();
+ if (defaultValue != null) {
+ writer.printf("| `%s`%n", defaultValue);
+ }
+ else {
+ writer.println("|");
+ }
+ writer.println();
+ }
+ writer.println("|===");
+ }
+
+ private void writeParameterDetails(PrintWriter writer, List parameters, String sectionId) {
+ for (Parameter parameter : parameters) {
+ String name = parameter.getName();
+ writer.println();
+ writer.println();
+ writer.printf("[[%s-%s]]%n", sectionId, name);
+ writer.printf("==== `%s`%n", name);
+ writer.println(parameter.getDescription());
+ writer.println();
+ writer.println("[cols=\"10h,90\"]");
+ writer.println("|===");
+ writer.println();
+ writeDetail(writer, "Name", name);
+ writeDetail(writer, "Type", parameter.getType());
+ writeOptionalDetail(writer, "Default value", parameter.getDefaultValue());
+ writeOptionalDetail(writer, "User property", parameter.getUserProperty());
+ writeOptionalDetail(writer, "Since", parameter.getSince());
+ writer.println("|===");
+ }
+ }
+
+ private void writeDetail(PrintWriter writer, String name, String value) {
+ writer.printf("| %s%n", name);
+ writer.printf("| `%s`%n", value);
+ writer.println();
+ }
+
+ private void writeOptionalDetail(PrintWriter writer, String name, String value) {
+ writer.printf("| %s%n", name);
+ if (value != null) {
+ writer.printf("| `%s`%n", value);
+ }
+ else {
+ writer.println("|");
+ }
+ writer.println();
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenExec.java b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenExec.java
new file mode 100644
index 0000000000..17694f11d1
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenExec.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2012-2020 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.mavenplugin;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.gradle.api.Project;
+import org.gradle.api.Task;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.tasks.Internal;
+import org.gradle.api.tasks.JavaExec;
+import org.gradle.api.tasks.TaskExecutionException;
+import org.gradle.process.internal.ExecException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A custom {@link JavaExec} {@link Task task} for running Maven.
+ *
+ * @author Andy Wilkinson
+ */
+public class MavenExec extends JavaExec {
+
+ private Logger log = LoggerFactory.getLogger(MavenExec.class);
+
+ private File projectDir;
+
+ public MavenExec() throws IOException {
+ setClasspath(mavenConfiguration(getProject()));
+ args("--batch-mode");
+ setMain("org.apache.maven.cli.MavenCli");
+ }
+
+ public void setProjectDir(File projectDir) {
+ this.projectDir = projectDir;
+ getInputs().file(new File(projectDir, "pom.xml"));
+ }
+
+ @Override
+ public void exec() {
+ workingDir(this.projectDir);
+ systemProperty("maven.multiModuleProjectDirectory", this.projectDir.getAbsolutePath());
+ try {
+ Path logFile = Files.createTempFile(getName(), ".log");
+ try {
+ args("--log-file", logFile.toFile().getAbsolutePath());
+ super.exec();
+ if (this.log.isInfoEnabled()) {
+ Files.readAllLines(logFile).forEach(this.log::info);
+ }
+ }
+ catch (ExecException ex) {
+ System.out.println("Exec exception! Dumping log");
+ Files.readAllLines(logFile).forEach(System.out::println);
+ throw ex;
+ }
+ }
+ catch (IOException ex) {
+ throw new TaskExecutionException(this, ex);
+ }
+ }
+
+ private Configuration mavenConfiguration(Project project) {
+ Configuration existing = project.getConfigurations().findByName("maven");
+ if (existing != null) {
+ return existing;
+ }
+ return project.getConfigurations().create("maven", (maven) -> {
+ maven.getDependencies().add(project.getDependencies().create("org.apache.maven:maven-embedder:3.6.2"));
+ maven.getDependencies().add(project.getDependencies().create("org.apache.maven:maven-compat:3.6.2"));
+ maven.getDependencies().add(project.getDependencies().create("org.slf4j:slf4j-simple:1.7.5"));
+ maven.getDependencies().add(
+ project.getDependencies().create("org.apache.maven.resolver:maven-resolver-connector-basic:1.4.1"));
+ maven.getDependencies().add(
+ project.getDependencies().create("org.apache.maven.resolver:maven-resolver-transport-http:1.4.1"));
+ });
+ }
+
+ @Internal
+ public File getProjectDir() {
+ return this.projectDir;
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenPluginPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenPluginPlugin.java
new file mode 100644
index 0000000000..08874b11aa
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/MavenPluginPlugin.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2019 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.mavenplugin;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+
+import io.spring.javaformat.formatter.FileFormatter;
+import org.gradle.api.DefaultTask;
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.Task;
+import org.gradle.api.plugins.JavaLibraryPlugin;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.plugins.JavaPluginConvention;
+import org.gradle.api.publish.PublishingExtension;
+import org.gradle.api.publish.maven.MavenPublication;
+import org.gradle.api.publish.maven.plugins.MavenPublishPlugin;
+import org.gradle.api.tasks.Copy;
+import org.gradle.api.tasks.JavaExec;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.SourceSet;
+import org.gradle.api.tasks.SourceSetContainer;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.api.tasks.TaskExecutionException;
+import org.gradle.api.tasks.bundling.Jar;
+import org.gradle.api.tasks.javadoc.Javadoc;
+import org.gradle.external.javadoc.StandardJavadocDocletOptions;
+
+import org.springframework.boot.build.DeployedPlugin;
+import org.springframework.boot.build.MavenRepositoryPlugin;
+import org.springframework.boot.build.test.IntegrationTestPlugin;
+
+/**
+ * Plugin for building Spring Boot's Maven Plugin.
+ *
+ * @author Andy Wilkinson
+ */
+public class MavenPluginPlugin implements Plugin {
+
+ @Override
+ public void apply(Project project) {
+ project.getPlugins().apply(JavaLibraryPlugin.class);
+ project.getPlugins().apply(MavenPublishPlugin.class);
+ project.getPlugins().apply(DeployedPlugin.class);
+ project.getPlugins().apply(MavenRepositoryPlugin.class);
+ project.getPlugins().apply(IntegrationTestPlugin.class);
+ Copy populateIntTestMavenRepository = project.getTasks().create("populateIntTestMavenRepository", Copy.class);
+ populateIntTestMavenRepository.setDestinationDir(project.getBuildDir());
+ populateIntTestMavenRepository.into("int-test-maven-repository", (copy) -> {
+ copy.from(project.getConfigurations().getByName(MavenRepositoryPlugin.MAVEN_REPOSITORY_CONFIGURATION_NAME));
+ copy.from(new File(project.getBuildDir(), "maven-repository"));
+ });
+ populateIntTestMavenRepository
+ .dependsOn(project.getTasks().getByName(MavenRepositoryPlugin.PUBLISH_TO_PROJECT_REPOSITORY_TASK_NAME));
+ configurePomPackaging(project);
+ MavenExec generateHelpMojo = configureMojoGenerationTasks(project);
+ MavenExec generatePluginDescriptor = configurePluginDescriptorGenerationTasks(project, generateHelpMojo);
+ DocumentPluginGoals documentPluginGoals = project.getTasks().create("documentPluginGoals",
+ DocumentPluginGoals.class);
+ documentPluginGoals.setPluginXml(generatePluginDescriptor.getOutputs().getFiles().getSingleFile());
+ documentPluginGoals.setOutputDir(new File(project.getBuildDir(), "docs/generated/goals/"));
+ documentPluginGoals.dependsOn(generatePluginDescriptor);
+ Jar jar = (Jar) project.getTasks().getByName(JavaPlugin.JAR_TASK_NAME);
+ includeDescriptorInJar(jar, generatePluginDescriptor);
+ includeHelpMojoInJar(jar, generateHelpMojo);
+ PrepareMavenBinaries prepareMavenBinaries = project.getTasks().create("prepareMavenBinaries",
+ PrepareMavenBinaries.class);
+ prepareMavenBinaries.setOutputDir(new File(project.getBuildDir(), "maven-binaries"));
+ project.getTasks().getByName(IntegrationTestPlugin.INT_TEST_TASK_NAME).dependsOn(populateIntTestMavenRepository,
+ prepareMavenBinaries);
+ }
+
+ private void configurePomPackaging(Project project) {
+ PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class);
+ publishing.getPublications().withType(MavenPublication.class,
+ (mavenPublication) -> mavenPublication.pom((pom) -> pom.setPackaging("maven-plugin")));
+ }
+
+ private MavenExec configureMojoGenerationTasks(Project project) {
+ File helpMojoDir = new File(project.getBuildDir(), "help-mojo");
+ Copy helpMojoInputs = createCopyHelpMojoInputs(project, helpMojoDir);
+ MavenExec generateHelpMojo = createGenerateHelpMojo(project, helpMojoDir);
+ generateHelpMojo.dependsOn(helpMojoInputs);
+ return generateHelpMojo;
+ }
+
+ private Copy createCopyHelpMojoInputs(Project project, File mavenDir) {
+ Copy mojoInputs = project.getTasks().create("copyHelpMojoInputs", Copy.class);
+ mojoInputs.setDestinationDir(mavenDir);
+ mojoInputs.from(new File(project.getProjectDir(), "src/maven/resources/pom.xml"),
+ (sync) -> sync.filter((input) -> input.replace("{{version}}", project.getVersion().toString())));
+ return mojoInputs;
+ }
+
+ private MavenExec createGenerateHelpMojo(Project project, File mavenDir) {
+ MavenExec generateHelpMojo = project.getTasks().create("generateHelpMojo", MavenExec.class);
+ generateHelpMojo.setProjectDir(mavenDir);
+ generateHelpMojo.args("org.apache.maven.plugins:maven-plugin-plugin:3.6.0:helpmojo");
+ generateHelpMojo.getOutputs().dir(new File(mavenDir, "target/generated-sources/plugin"));
+ return generateHelpMojo;
+ }
+
+ private MavenExec configurePluginDescriptorGenerationTasks(Project project, MavenExec generateHelpMojo) {
+ File pluginDescriptorDir = new File(project.getBuildDir(), "plugin-descriptor");
+ SourceSetContainer sourceSets = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets();
+ SourceSet mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
+ File generatedHelpMojoDir = new File(project.getBuildDir(), "generated/sources/helpMojo");
+ project.getTasks().withType(Javadoc.class,
+ (javadoc) -> ((StandardJavadocDocletOptions) javadoc.getOptions()).addMultilineStringsOption("tag")
+ .setValue(Arrays.asList("goal:X", "requiresProject:X", "threadSafe:X")));
+ FormatHelpMojoSource copyFormattedHelpMojoSource = project.getTasks().create("copyFormattedHelpMojoSource",
+ FormatHelpMojoSource.class);
+ copyFormattedHelpMojoSource.setGenerator(generateHelpMojo);
+ copyFormattedHelpMojoSource.setOutputDir(generatedHelpMojoDir);
+ mainSourceSet.getAllJava().srcDir(generatedHelpMojoDir);
+ project.getTasks().getByName(mainSourceSet.getCompileJavaTaskName()).dependsOn(copyFormattedHelpMojoSource);
+ Copy pluginDescriptorInputs = createCopyPluginDescriptorInputs(project, pluginDescriptorDir, mainSourceSet);
+ pluginDescriptorInputs.dependsOn(mainSourceSet.getClassesTaskName());
+ MavenExec generatePluginDescriptor = createGeneratePluginDescriptor(project, pluginDescriptorDir);
+ generatePluginDescriptor.dependsOn(pluginDescriptorInputs);
+ return generatePluginDescriptor;
+ }
+
+ private Copy createCopyPluginDescriptorInputs(Project project, File destination, SourceSet sourceSet) {
+ Copy pluginDescriptorInputs = project.getTasks().create("copyPluginDescriptorInputs", Copy.class);
+ pluginDescriptorInputs.setDestinationDir(destination);
+ pluginDescriptorInputs.from(new File(project.getProjectDir(), "src/maven/resources/pom.xml"),
+ (sync) -> sync.filter((input) -> input.replace("{{version}}", project.getVersion().toString())));
+ pluginDescriptorInputs.from(sourceSet.getOutput().getClassesDirs(), (sync) -> sync.into("target/classes"));
+ pluginDescriptorInputs.from(sourceSet.getAllJava().getSrcDirs(), (sync) -> sync.into("src/main/java"));
+ return pluginDescriptorInputs;
+ }
+
+ private MavenExec createGeneratePluginDescriptor(Project project, File mavenDir) {
+ MavenExec generatePluginDescriptor = project.getTasks().create("generatePluginDescriptor", MavenExec.class);
+ generatePluginDescriptor.args("org.apache.maven.plugins:maven-plugin-plugin:3.6.0:descriptor");
+ generatePluginDescriptor.getOutputs().file(new File(mavenDir, "target/classes/META-INF/maven/plugin.xml"));
+ generatePluginDescriptor.getInputs().dir(new File(mavenDir, "target/classes/org"));
+ generatePluginDescriptor.setProjectDir(mavenDir);
+ return generatePluginDescriptor;
+ }
+
+ private void includeDescriptorInJar(Jar jar, JavaExec generatePluginDescriptor) {
+ jar.from(generatePluginDescriptor, (copy) -> copy.into("META-INF/maven/"));
+ jar.dependsOn(generatePluginDescriptor);
+ }
+
+ private void includeHelpMojoInJar(Jar jar, JavaExec generateHelpMojo) {
+ jar.from(generateHelpMojo);
+ jar.dependsOn(generateHelpMojo);
+ }
+
+ public static class FormatHelpMojoSource extends DefaultTask {
+
+ private Task generator;
+
+ private File outputDir;
+
+ void setGenerator(Task generator) {
+ this.generator = generator;
+ getInputs().files(this.generator);
+ }
+
+ @OutputDirectory
+ public File getOutputDir() {
+ return this.outputDir;
+ }
+
+ void setOutputDir(File outputDir) {
+ this.outputDir = outputDir;
+ }
+
+ @TaskAction
+ void syncAndFormat() {
+ FileFormatter fileFormatter = new FileFormatter();
+ for (File output : this.generator.getOutputs().getFiles()) {
+ fileFormatter.formatFiles(getProject().fileTree(output), StandardCharsets.UTF_8).forEach((fileEdit) -> {
+ Path relativePath = output.toPath().relativize(fileEdit.getFile().toPath());
+ Path outputLocation = this.outputDir.toPath().resolve(relativePath);
+ try {
+ Files.createDirectories(outputLocation.getParent());
+ Files.write(outputLocation, fileEdit.getFormattedContent().getBytes(StandardCharsets.UTF_8));
+ }
+ catch (Exception ex) {
+ throw new TaskExecutionException(this, ex);
+ }
+ });
+ }
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/PluginXmlParser.java b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/PluginXmlParser.java
new file mode 100644
index 0000000000..6269923df2
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/PluginXmlParser.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2019 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.mavenplugin;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+/**
+ * A parser for a Maven plugin's {@code plugin.xml} file.
+ *
+ * @author Andy Wilkinson
+ */
+class PluginXmlParser {
+
+ private final XPath xpath;
+
+ PluginXmlParser() {
+ this.xpath = XPathFactory.newInstance().newXPath();
+ }
+
+ Plugin parse(File pluginXml) {
+ try {
+ Node root = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(pluginXml);
+ List mojos = parseMojos(root);
+ return new Plugin(textAt("//plugin/groupId", root), textAt("//plugin/artifactId", root),
+ textAt("//plugin/version", root), textAt("//plugin/goalPrefix", root), mojos);
+ }
+ catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private String textAt(String path, Node source) throws XPathExpressionException {
+ String text = this.xpath.evaluate(path + "/text()", source);
+ return (text.length() == 0) ? null : text;
+ }
+
+ private List parseMojos(Node plugin) throws XPathExpressionException {
+ List mojos = new ArrayList();
+ for (Node mojoNode : nodesAt("//plugin/mojos/mojo", plugin)) {
+ mojos.add(new Mojo(textAt("goal", mojoNode), format(textAt("description", mojoNode)),
+ parseParameters(mojoNode)));
+ }
+ return mojos;
+ }
+
+ private Iterable nodesAt(String path, Node source) throws XPathExpressionException {
+ return IterableNodeList.of((NodeList) this.xpath.evaluate(path, source, XPathConstants.NODESET));
+ }
+
+ private List parseParameters(Node mojoNode) throws XPathExpressionException {
+ Map defaultValues = new HashMap<>();
+ Map userProperties = new HashMap<>();
+ for (Node parameterConfigurationNode : nodesAt("configuration/*", mojoNode)) {
+ String userProperty = parameterConfigurationNode.getTextContent();
+ if (userProperty != null && userProperty.length() > 0) {
+ userProperties.put(parameterConfigurationNode.getNodeName(),
+ userProperty.replace("${", "`").replace("}", "`"));
+ }
+ Node defaultValueAttribute = parameterConfigurationNode.getAttributes().getNamedItem("default-value");
+ if (defaultValueAttribute != null && defaultValueAttribute.getTextContent().length() > 0) {
+ defaultValues.put(parameterConfigurationNode.getNodeName(), defaultValueAttribute.getTextContent());
+ }
+ }
+ List parameters = new ArrayList<>();
+ for (Node parameterNode : nodesAt("parameters/parameter", mojoNode)) {
+ parameters.add(parseParameter(parameterNode, defaultValues, userProperties));
+ }
+ return parameters;
+ }
+
+ private Parameter parseParameter(Node parameterNode, Map defaultValues,
+ Map userProperties) throws XPathExpressionException {
+ Parameter parameter = new Parameter(textAt("name", parameterNode), textAt("type", parameterNode),
+ booleanAt("required", parameterNode), booleanAt("editable", parameterNode),
+ format(textAt("description", parameterNode)), defaultValues.get(textAt("name", parameterNode)),
+ userProperties.get(textAt("name", parameterNode)), textAt("since", parameterNode));
+ return parameter;
+ }
+
+ private boolean booleanAt(String path, Node node) throws XPathExpressionException {
+ return Boolean.valueOf(textAt(path, node));
+ }
+
+ private String format(String input) {
+ return input.replace("", "`").replace("
", "`").replace("<", "<").replace(">", ">")
+ .replace(" ", " ").replace("\n", " ").replace(""", "\"").replaceAll("\\{@code (.*?)\\}", "`$1`")
+ .replaceAll("\\{@link (.*?)\\}", "`$1`").replaceAll("\\{@literal (.*?)\\}", "`$1`")
+ .replaceAll("(.*?) ", "\\$1[\\$2]");
+ }
+
+ private static final class IterableNodeList implements Iterable {
+
+ private final NodeList nodeList;
+
+ private IterableNodeList(NodeList nodeList) {
+ this.nodeList = nodeList;
+ }
+
+ private static Iterable of(NodeList nodeList) {
+ return new IterableNodeList(nodeList);
+ }
+
+ @Override
+ public Iterator iterator() {
+
+ return new Iterator() {
+
+ private int index = 0;
+
+ @Override
+ public boolean hasNext() {
+ return this.index < IterableNodeList.this.nodeList.getLength();
+ }
+
+ @Override
+ public Node next() {
+ return IterableNodeList.this.nodeList.item(this.index++);
+ }
+
+ };
+ }
+
+ }
+
+ static final class Plugin {
+
+ private final String groupId;
+
+ private final String artifactId;
+
+ private final String version;
+
+ private final String goalPrefix;
+
+ private final List mojos;
+
+ private Plugin(String groupId, String artifactId, String version, String goalPrefix, List mojos) {
+ this.groupId = groupId;
+ this.artifactId = artifactId;
+ this.version = version;
+ this.goalPrefix = goalPrefix;
+ this.mojos = mojos;
+ }
+
+ String getGroupId() {
+ return this.groupId;
+ }
+
+ String getArtifactId() {
+ return this.artifactId;
+ }
+
+ String getVersion() {
+ return this.version;
+ }
+
+ String getGoalPrefix() {
+ return this.goalPrefix;
+ }
+
+ List getMojos() {
+ return this.mojos;
+ }
+
+ }
+
+ static final class Mojo {
+
+ private final String goal;
+
+ private final String description;
+
+ private final List parameters;
+
+ private Mojo(String goal, String description, List parameters) {
+ this.goal = goal;
+ this.description = description;
+ this.parameters = parameters;
+ }
+
+ String getGoal() {
+ return this.goal;
+ }
+
+ String getDescription() {
+ return this.description;
+ }
+
+ List getParameters() {
+ return this.parameters;
+ }
+
+ }
+
+ static final class Parameter {
+
+ private final String name;
+
+ private final String type;
+
+ private final boolean required;
+
+ private final boolean editable;
+
+ private final String description;
+
+ private final String defaultValue;
+
+ private final String userProperty;
+
+ private final String since;
+
+ private Parameter(String name, String type, boolean required, boolean editable, String description,
+ String defaultValue, String userProperty, String since) {
+ this.name = name;
+ this.type = type;
+ this.required = required;
+ this.editable = editable;
+ this.description = description;
+ this.defaultValue = defaultValue;
+ this.userProperty = userProperty;
+ this.since = since;
+ }
+
+ String getName() {
+ return this.name;
+ }
+
+ String getType() {
+ return this.type;
+ }
+
+ boolean isRequired() {
+ return this.required;
+ }
+
+ boolean isEditable() {
+ return this.editable;
+ }
+
+ String getDescription() {
+ return this.description;
+ }
+
+ String getDefaultValue() {
+ return this.defaultValue;
+ }
+
+ String getUserProperty() {
+ return this.userProperty;
+ }
+
+ String getSince() {
+ return this.since;
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/PrepareMavenBinaries.java b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/PrepareMavenBinaries.java
new file mode 100644
index 0000000000..e4f18ece6b
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/mavenplugin/PrepareMavenBinaries.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019 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.mavenplugin;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.gradle.api.DefaultTask;
+import org.gradle.api.Task;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.tasks.Input;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.TaskAction;
+
+/**
+ * {@link Task} to make Maven binaries available for integration testing.
+ *
+ * @author Andy Wilkinson
+ */
+public class PrepareMavenBinaries extends DefaultTask {
+
+ private Set versions = new LinkedHashSet<>();
+
+ private File outputDir;
+
+ @OutputDirectory
+ public File getOutputDir() {
+ return this.outputDir;
+ }
+
+ public void setOutputDir(File outputDir) {
+ this.outputDir = outputDir;
+ }
+
+ @Input
+ public Set getVersions() {
+ return this.versions;
+ }
+
+ public void versions(String... versions) {
+ this.versions.addAll(Arrays.asList(versions));
+ }
+
+ @TaskAction
+ public void prepareBinaries() {
+ for (String version : this.versions) {
+ Configuration configuration = getProject().getConfigurations().detachedConfiguration(
+ getProject().getDependencies().create("org.apache.maven:apache-maven:" + version + ":bin@zip"));
+ getProject().copy(
+ (copy) -> copy.into(this.outputDir).from(getProject().zipTree(configuration.getSingleFile())));
+ }
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/optional/OptionalDependenciesPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/optional/OptionalDependenciesPlugin.java
new file mode 100644
index 0000000000..0f622fcee8
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/optional/OptionalDependenciesPlugin.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2012-2020 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.optional;
+
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.attributes.Usage;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.plugins.JavaPluginConvention;
+import org.gradle.api.tasks.SourceSetContainer;
+import org.gradle.api.tasks.javadoc.Javadoc;
+import org.gradle.plugins.ide.eclipse.EclipsePlugin;
+import org.gradle.plugins.ide.eclipse.model.EclipseModel;
+
+/**
+ * A {@code Plugin} that adds support for Maven-style optional dependencies. Creates a new
+ * {@code optional} configuration. The {@code optional} configuration is part of the
+ * project's compile and runtime classpath's but does not affect the classpath of
+ * dependent projects.
+ *
+ * @author Andy Wilkinson
+ */
+public class OptionalDependenciesPlugin implements Plugin {
+
+ /**
+ * Name of the {@code optional} configuration.
+ */
+ public static final String OPTIONAL_CONFIGURATION_NAME = "optional";
+
+ @Override
+ public void apply(Project project) {
+ Configuration optional = project.getConfigurations().create("optional");
+ optional.attributes((attributes) -> attributes.attribute(Usage.USAGE_ATTRIBUTE,
+ project.getObjects().named(Usage.class, Usage.JAVA_RUNTIME)));
+ project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
+ SourceSetContainer sourceSets = project.getConvention().getPlugin(JavaPluginConvention.class)
+ .getSourceSets();
+ sourceSets.all((sourceSet) -> {
+ sourceSet.setCompileClasspath(sourceSet.getCompileClasspath().plus(optional));
+ sourceSet.setRuntimeClasspath(sourceSet.getRuntimeClasspath().plus(optional));
+ });
+ project.getTasks().withType(Javadoc.class)
+ .all((javadoc) -> javadoc.setClasspath(javadoc.getClasspath().plus(optional)));
+ });
+ project.getPlugins().withType(EclipsePlugin.class,
+ (eclipePlugin) -> project.getExtensions().getByType(EclipseModel.class)
+ .classpath((classpath) -> classpath.getPlusConfigurations().add(optional)));
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/starters/DocumentStarters.java b/buildSrc/src/main/java/org/springframework/boot/build/starters/DocumentStarters.java
new file mode 100644
index 0000000000..a5cdf124d4
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/starters/DocumentStarters.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2019 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.starters;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.gradle.api.Task;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.internal.AbstractTask;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputDirectory;
+import org.gradle.api.tasks.TaskAction;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link Task} to document all starter projects.
+ *
+ * @author Andy Wilkinson
+ */
+public class DocumentStarters extends AbstractTask {
+
+ private final Configuration starters;
+
+ private File outputDir;
+
+ public DocumentStarters() {
+ this.starters = getProject().getConfigurations().create("starters");
+ getProject().getGradle().projectsEvaluated((gradle) -> {
+ gradle.allprojects((project) -> {
+ if (project.getPlugins().hasPlugin(StarterPlugin.class)) {
+ Map dependency = new HashMap<>();
+ dependency.put("path", project.getPath());
+ dependency.put("configuration", "starterMetadata");
+ this.starters.getDependencies().add(project.getDependencies().project(dependency));
+ }
+ });
+ });
+ }
+
+ @OutputDirectory
+ public File getOutputDir() {
+ return this.outputDir;
+ }
+
+ public void setOutputDir(File outputDir) {
+ this.outputDir = outputDir;
+ }
+
+ @InputFiles
+ public FileCollection getStarters() {
+ return this.starters;
+ }
+
+ @TaskAction
+ void documentStarters() {
+ Set starters = this.starters.getFiles().stream().map(this::loadStarter)
+ .collect(Collectors.toCollection(TreeSet::new));
+ writeTable("application-starters", starters.stream().filter(Starter::isApplication));
+ writeTable("production-starters", starters.stream().filter(Starter::isProduction));
+ writeTable("technical-starters", starters.stream().filter(Starter::isTechnical));
+ }
+
+ private Starter loadStarter(File metadata) {
+ Properties properties = new Properties();
+ try (FileReader reader = new FileReader(metadata)) {
+ properties.load(reader);
+ return new Starter(properties.getProperty("name"), properties.getProperty("description"),
+ StringUtils.commaDelimitedListToSet(properties.getProperty("dependencies")));
+ }
+ catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private void writeTable(String name, Stream starters) {
+ File output = new File(this.outputDir, name + ".adoc");
+ output.getParentFile().mkdirs();
+ try (PrintWriter writer = new PrintWriter(new FileWriter(output))) {
+ writer.println("|===");
+ writer.println("| Name | Description");
+ starters.forEach((starter) -> {
+ writer.println();
+ writer.printf("| [[%s]]`%s`%n", starter.name, starter.name);
+ writer.printf("| %s%n", postProcessDescription(starter.description));
+ });
+ writer.println("|===");
+ }
+ catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private String postProcessDescription(String description) {
+ return addStarterCrossLinks(description);
+ }
+
+ private String addStarterCrossLinks(String input) {
+ return input.replaceAll("(spring-boot-starter[A-Za-z-]*)", "<<$1,`$1`>>");
+ }
+
+ private static final class Starter implements Comparable {
+
+ private final String name;
+
+ private final String description;
+
+ private final Set dependencies;
+
+ private Starter(String name, String description, Set dependencies) {
+ this.name = name;
+ this.description = description;
+ this.dependencies = dependencies;
+ }
+
+ private boolean isProduction() {
+ return this.name.equals("spring-boot-starter-actuator");
+ }
+
+ private boolean isTechnical() {
+ return !Arrays.asList("spring-boot-starter", "spring-boot-starter-test").contains(this.name)
+ && !isProduction() && !this.dependencies.contains("spring-boot-starter");
+ }
+
+ private boolean isApplication() {
+ return !isProduction() && !isTechnical();
+ }
+
+ @Override
+ public int compareTo(Starter other) {
+ return this.name.compareTo(other.name);
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/starters/StarterMetadata.java b/buildSrc/src/main/java/org/springframework/boot/build/starters/StarterMetadata.java
new file mode 100644
index 0000000000..75e09c1273
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/starters/StarterMetadata.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2012-2020 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.starters;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Properties;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+
+import org.gradle.api.Task;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.ResolvedArtifact;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.internal.AbstractTask;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+
+/**
+ * A {@link Task} for generating metadata that describes a starter.
+ *
+ * @author Andy Wilkinson
+ */
+public class StarterMetadata extends AbstractTask {
+
+ private Configuration dependencies;
+
+ private File destination;
+
+ public StarterMetadata() {
+ getInputs().property("name", (Callable) () -> getProject().getName());
+ getInputs().property("description", (Callable) () -> getProject().getDescription());
+ }
+
+ @InputFiles
+ public FileCollection getDependencies() {
+ return this.dependencies;
+ }
+
+ public void setDependencies(Configuration dependencies) {
+ this.dependencies = dependencies;
+ }
+
+ @OutputFile
+ public File getDestination() {
+ return this.destination;
+ }
+
+ public void setDestination(File destination) {
+ this.destination = destination;
+ }
+
+ @TaskAction
+ void generateMetadata() throws IOException {
+ Properties properties = new Properties();
+ properties.setProperty("name", getProject().getName());
+ properties.setProperty("description", getProject().getDescription());
+ properties.setProperty("dependencies", String.join(",", this.dependencies.getResolvedConfiguration()
+ .getResolvedArtifacts().stream().map(ResolvedArtifact::getName).collect(Collectors.toSet())));
+ this.destination.getParentFile().mkdirs();
+ try (FileWriter writer = new FileWriter(this.destination)) {
+ properties.store(writer, null);
+ }
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/starters/StarterPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/starters/StarterPlugin.java
new file mode 100644
index 0000000000..3ad6a3f714
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/starters/StarterPlugin.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2019 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.starters;
+
+import java.io.File;
+
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.artifacts.ConfigurationContainer;
+import org.gradle.api.plugins.JavaBasePlugin;
+import org.gradle.api.plugins.JavaLibraryPlugin;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.plugins.PluginContainer;
+
+import org.springframework.boot.build.ConventionsPlugin;
+import org.springframework.boot.build.DeployedPlugin;
+import org.springframework.boot.build.classpath.CheckClasspathForConflicts;
+import org.springframework.boot.build.classpath.CheckClasspathForProhibitedDependencies;
+import org.springframework.util.StringUtils;
+
+/**
+ * A {@link Plugin} for a starter project.
+ *
+ * @author Andy Wilkinson
+ */
+public class StarterPlugin implements Plugin {
+
+ @Override
+ public void apply(Project project) {
+ PluginContainer plugins = project.getPlugins();
+ plugins.apply(DeployedPlugin.class);
+ plugins.apply(JavaLibraryPlugin.class);
+ plugins.apply(ConventionsPlugin.class);
+ StarterMetadata starterMetadata = project.getTasks().create("starterMetadata", StarterMetadata.class);
+ ConfigurationContainer configurations = project.getConfigurations();
+ Configuration runtimeClasspath = configurations.getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME);
+ starterMetadata.setDependencies(runtimeClasspath);
+ File destination = new File(project.getBuildDir(), "starter-metadata.properties");
+ starterMetadata.setDestination(destination);
+ configurations.create("starterMetadata");
+ project.getArtifacts().add("starterMetadata", project.provider(starterMetadata::getDestination),
+ (artifact) -> artifact.builtBy(starterMetadata));
+ createClasspathConflictsCheck(runtimeClasspath, project);
+ createProhibitedDependenciesCheck(runtimeClasspath, project);
+ }
+
+ private void createClasspathConflictsCheck(Configuration classpath, Project project) {
+ CheckClasspathForConflicts checkClasspathForConflicts = project.getTasks().create(
+ "check" + StringUtils.capitalize(classpath.getName() + "ForConflicts"),
+ CheckClasspathForConflicts.class);
+ checkClasspathForConflicts.setClasspath(classpath);
+ project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForConflicts);
+ }
+
+ private void createProhibitedDependenciesCheck(Configuration classpath, Project project) {
+ CheckClasspathForProhibitedDependencies checkClasspathForProhibitedDependencies = project.getTasks().create(
+ "check" + StringUtils.capitalize(classpath.getName() + "ForProhibitedDependencies"),
+ CheckClasspathForProhibitedDependencies.class);
+ checkClasspathForProhibitedDependencies.setClasspath(classpath);
+ project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForProhibitedDependencies);
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/test/IntegrationTestPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/test/IntegrationTestPlugin.java
new file mode 100644
index 0000000000..58ac2fb33f
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/test/IntegrationTestPlugin.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2012-2020 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.test;
+
+import org.gradle.api.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.plugins.JavaPlugin;
+import org.gradle.api.plugins.JavaPluginConvention;
+import org.gradle.api.tasks.SourceSet;
+import org.gradle.api.tasks.SourceSetContainer;
+import org.gradle.api.tasks.testing.Test;
+import org.gradle.language.base.plugins.LifecycleBasePlugin;
+import org.gradle.plugins.ide.eclipse.EclipsePlugin;
+import org.gradle.plugins.ide.eclipse.model.EclipseModel;
+
+/**
+ * A {@Plugin} to configure integration testing support in a {@link Project}.
+ *
+ * @author Andy Wilkinson
+ */
+public class IntegrationTestPlugin implements Plugin {
+
+ /**
+ * Name of the {@code intTest} task.
+ */
+ public static String INT_TEST_TASK_NAME = "intTest";
+
+ /**
+ * Name of the {@code intTest} source set.
+ */
+ public static String INT_TEST_SOURCE_SET_NAME = "intTest";
+
+ @Override
+ public void apply(Project project) {
+ project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> this.configureIntegrationTesting(project));
+ }
+
+ private void configureIntegrationTesting(Project project) {
+ SourceSet intTestSourceSet = createSourceSet(project);
+ Test intTest = createTestTask(project, intTestSourceSet);
+ project.getTasks().getByName(LifecycleBasePlugin.CHECK_TASK_NAME).dependsOn(intTest);
+ project.getPlugins().withType(EclipsePlugin.class, (eclipsePlugin) -> {
+ EclipseModel eclipse = project.getExtensions().getByType(EclipseModel.class);
+ eclipse.classpath((classpath) -> classpath.getPlusConfigurations().add(
+ project.getConfigurations().getByName(intTestSourceSet.getRuntimeClasspathConfigurationName())));
+ });
+ }
+
+ private SourceSet createSourceSet(Project project) {
+ SourceSetContainer sourceSets = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets();
+ SourceSet intTestSourceSet = sourceSets.create(INT_TEST_SOURCE_SET_NAME);
+ SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
+ intTestSourceSet.setCompileClasspath(intTestSourceSet.getCompileClasspath().plus(main.getOutput()));
+ intTestSourceSet.setRuntimeClasspath(intTestSourceSet.getRuntimeClasspath().plus(main.getOutput()));
+ return intTestSourceSet;
+ }
+
+ private Test createTestTask(Project project, SourceSet intTestSourceSet) {
+ Test intTest = project.getTasks().create(INT_TEST_TASK_NAME, Test.class);
+ intTest.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
+ intTest.setDescription("Runs integration tests.");
+ intTest.setTestClassesDirs(intTestSourceSet.getOutput().getClassesDirs());
+ intTest.setClasspath(intTestSourceSet.getRuntimeClasspath());
+ intTest.shouldRunAfter(JavaPlugin.TEST_TASK_NAME);
+ return intTest;
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/test/autoconfigure/DocumentTestSlices.java b/buildSrc/src/main/java/org/springframework/boot/build/test/autoconfigure/DocumentTestSlices.java
new file mode 100644
index 0000000000..6643b9fcf1
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/test/autoconfigure/DocumentTestSlices.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2012-2020 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.test.autoconfigure;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Properties;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import org.gradle.api.Task;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.internal.AbstractTask;
+import org.gradle.api.tasks.InputFiles;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * {@link Task} used to document test slices.
+ *
+ * @author Andy Wilkinson
+ */
+public class DocumentTestSlices extends AbstractTask {
+
+ private FileCollection testSlices;
+
+ private File outputFile;
+
+ @InputFiles
+ public FileCollection getTestSlices() {
+ return this.testSlices;
+ }
+
+ public void setTestSlices(FileCollection testSlices) {
+ this.testSlices = testSlices;
+ }
+
+ @OutputFile
+ public File getOutputFile() {
+ return this.outputFile;
+ }
+
+ public void setOutputFile(File outputFile) {
+ this.outputFile = outputFile;
+ }
+
+ @TaskAction
+ void documentTestSlices() throws IOException {
+ Set testSlices = readTestSlices();
+ writeTable(testSlices);
+ }
+
+ @SuppressWarnings("unchecked")
+ private Set readTestSlices() throws IOException {
+ Set testSlices = new TreeSet<>();
+ for (File metadataFile : this.testSlices) {
+ Properties metadata = new Properties();
+ try (Reader reader = new FileReader(metadataFile)) {
+ metadata.load(reader);
+ }
+ for (String name : Collections.list((Enumeration) metadata.propertyNames())) {
+ testSlices.add(new TestSlice(name,
+ new TreeSet<>(StringUtils.commaDelimitedListToSet(metadata.getProperty(name)))));
+ }
+ }
+ return testSlices;
+ }
+
+ private void writeTable(Set testSlices) throws IOException {
+ this.outputFile.getParentFile().mkdirs();
+ try (PrintWriter writer = new PrintWriter(new FileWriter(this.outputFile))) {
+ writer.println("[cols=\"d,a\"]");
+ writer.println("|===");
+ writer.println("| Test slice | Imported auto-configuration");
+ for (TestSlice testSlice : testSlices) {
+ writer.println();
+ writer.printf("| `@%s`%n", testSlice.className);
+ writer.println("| ");
+ for (String importedAutoConfiguration : testSlice.importedAutoConfigurations) {
+ writer.printf("`%s`%n", importedAutoConfiguration);
+ }
+ }
+ writer.println("|===");
+ }
+ }
+
+ private static final class TestSlice implements Comparable {
+
+ private final String className;
+
+ private final SortedSet importedAutoConfigurations;
+
+ private TestSlice(String className, SortedSet importedAutoConfigurations) {
+ this.className = ClassUtils.getShortName(className);
+ this.importedAutoConfigurations = importedAutoConfigurations;
+ }
+
+ @Override
+ public int compareTo(TestSlice other) {
+ return this.className.compareTo(other.className);
+ }
+
+ }
+
+}
diff --git a/buildSrc/src/main/java/org/springframework/boot/build/test/autoconfigure/TestSliceMetadata.java b/buildSrc/src/main/java/org/springframework/boot/build/test/autoconfigure/TestSliceMetadata.java
new file mode 100644
index 0000000000..1a1c5ab9de
--- /dev/null
+++ b/buildSrc/src/main/java/org/springframework/boot/build/test/autoconfigure/TestSliceMetadata.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2012-2020 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.test.autoconfigure;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Properties;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.Callable;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.gradle.api.Task;
+import org.gradle.api.artifacts.Configuration;
+import org.gradle.api.file.FileCollection;
+import org.gradle.api.internal.AbstractTask;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.SourceSet;
+import org.gradle.api.tasks.TaskAction;
+
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
+import org.springframework.util.StringUtils;
+
+/**
+ * A {@link Task} for generating metadata describing a project's test slices.
+ *
+ * @author Andy Wilkinson
+ */
+public class TestSliceMetadata extends AbstractTask {
+
+ private SourceSet sourceSet;
+
+ private File outputFile;
+
+ public TestSliceMetadata() {
+ getInputs().dir((Callable) () -> this.sourceSet.getOutput().getResourcesDir());
+ getInputs().files((Callable) () -> this.sourceSet.getOutput().getClassesDirs());
+ }
+
+ public void setSourceSet(SourceSet sourceSet) {
+ this.sourceSet = sourceSet;
+ }
+
+ @OutputFile
+ public File getOutputFile() {
+ return this.outputFile;
+ }
+
+ public void setOutputFile(File outputFile) {
+ this.outputFile = outputFile;
+ Configuration testSliceMetadata = getProject().getConfigurations().maybeCreate("testSliceMetadata");
+ getProject().getArtifacts().add(testSliceMetadata.getName(),
+ getProject().provider((Callable) this::getOutputFile), (artifact) -> artifact.builtBy(this));
+ }
+
+ @TaskAction
+ void documentTestSlices() throws IOException {
+ Properties testSlices = readTestSlices();
+ getOutputFile().getParentFile().mkdirs();
+ try (FileWriter writer = new FileWriter(getOutputFile())) {
+ testSlices.store(writer, null);
+ }
+ }
+
+ private Properties readTestSlices() throws IOException {
+ Properties testSlices = new Properties();
+ try (URLClassLoader classLoader = new URLClassLoader(
+ StreamSupport.stream(this.sourceSet.getRuntimeClasspath().spliterator(), false).map(this::toURL)
+ .toArray(URL[]::new))) {
+ MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(classLoader);
+ Properties springFactories = readSpringFactories(
+ new File(this.sourceSet.getOutput().getResourcesDir(), "META-INF/spring.factories"));
+ for (File classesDir : this.sourceSet.getOutput().getClassesDirs()) {
+ addTestSlices(testSlices, classesDir, metadataReaderFactory, springFactories);
+ }
+ }
+ return testSlices;
+ }
+
+ private URL toURL(File file) {
+ try {
+ return file.toURI().toURL();
+ }
+ catch (MalformedURLException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private Properties readSpringFactories(File file) throws IOException {
+ Properties springFactories = new Properties();
+ try (Reader in = new FileReader(file)) {
+ springFactories.load(in);
+ }
+ return springFactories;
+ }
+
+ private void addTestSlices(Properties testSlices, File classesDir, MetadataReaderFactory metadataReaderFactory,
+ Properties springFactories) throws IOException {
+ try (Stream classes = Files.walk(classesDir.toPath())) {
+ classes.filter((path) -> path.toString().endsWith("Test.class"))
+ .map((path) -> getMetadataReader(path, metadataReaderFactory))
+ .filter((metadataReader) -> metadataReader.getClassMetadata().isAnnotation())
+ .forEach((metadataReader) -> addTestSlice(testSlices, springFactories, metadataReader));
+ }
+
+ }
+
+ private MetadataReader getMetadataReader(Path path, MetadataReaderFactory metadataReaderFactory) {
+ try {
+ return metadataReaderFactory.getMetadataReader(new FileSystemResource(path));
+ }
+ catch (IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private void addTestSlice(Properties testSlices, Properties springFactories, MetadataReader metadataReader) {
+ testSlices.setProperty(metadataReader.getClassMetadata().getClassName(),
+ StringUtils.collectionToCommaDelimitedString(
+ getImportedAutoConfiguration(springFactories, metadataReader.getAnnotationMetadata())));
+ }
+
+ private SortedSet getImportedAutoConfiguration(Properties springFactories,
+ AnnotationMetadata annotationMetadata) {
+ Stream importers = findMetaImporters(annotationMetadata);
+ if (annotationMetadata.isAnnotated("org.springframework.boot.autoconfigure.ImportAutoConfiguration")) {
+ importers = Stream.concat(importers, Stream.of(annotationMetadata.getClassName()));
+ }
+ return importers.flatMap(
+ (importer) -> StringUtils.commaDelimitedListToSet(springFactories.getProperty(importer)).stream())
+ .collect(Collectors.toCollection(TreeSet::new));
+ }
+
+ private Stream findMetaImporters(AnnotationMetadata annotationMetadata) {
+ return annotationMetadata.getAnnotationTypes().stream()
+ .filter((annotationType) -> isAutoConfigurationImporter(annotationType, annotationMetadata));
+ }
+
+ private boolean isAutoConfigurationImporter(String annotationType, AnnotationMetadata metadata) {
+ return metadata.getMetaAnnotationTypes(annotationType)
+ .contains("org.springframework.boot.autoconfigure.ImportAutoConfiguration");
+ }
+
+}
diff --git a/buildSrc/src/main/resources/effective-bom-settings.xml b/buildSrc/src/main/resources/effective-bom-settings.xml
new file mode 100644
index 0000000000..d67307453c
--- /dev/null
+++ b/buildSrc/src/main/resources/effective-bom-settings.xml
@@ -0,0 +1,24 @@
+
+ localRepositoryPath
+
+
+ spring-repositories
+
+ true
+
+
+
+ spring-snapshot
+ https://repo.spring.io/snapshot
+
+ true
+
+
+
+ spring-milestone
+ https://repo.spring.io/milestone
+
+
+
+
+
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/assertj/NodeAssert.java b/buildSrc/src/test/java/org/springframework/boot/build/assertj/NodeAssert.java
new file mode 100644
index 0000000000..452fafaf6f
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/assertj/NodeAssert.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2012-2020 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.assertj;
+
+import java.io.File;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import org.assertj.core.api.AbstractAssert;
+import org.assertj.core.api.AssertProvider;
+import org.assertj.core.api.StringAssert;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+
+/**
+ * AssertJ {@link AssertProvider} for {@link Node} assertions.
+ *
+ * @author Andy Wilkinson
+ */
+public class NodeAssert extends AbstractAssert implements AssertProvider {
+
+ private static final DocumentBuilderFactory FACTORY = DocumentBuilderFactory.newInstance();
+
+ private final XPathFactory xpathFactory = XPathFactory.newInstance();
+
+ private final XPath xpath = this.xpathFactory.newXPath();
+
+ public NodeAssert(File xmlFile) {
+ this(read(xmlFile));
+ }
+
+ public NodeAssert(Node actual) {
+ super(actual, NodeAssert.class);
+ }
+
+ private static Document read(File xmlFile) {
+ try {
+ return FACTORY.newDocumentBuilder().parse(xmlFile);
+ }
+ catch (Exception ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public NodeAssert nodeAtPath(String xpath) {
+ try {
+ return new NodeAssert((Node) this.xpath.evaluate(xpath, this.actual, XPathConstants.NODE));
+ }
+ catch (XPathExpressionException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ public StringAssert textAtPath(String xpath) {
+ try {
+ return new StringAssert(
+ (String) this.xpath.evaluate(xpath + "/text()", this.actual, XPathConstants.STRING));
+ }
+ catch (XPathExpressionException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ public NodeAssert assertThat() {
+ return this;
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/bom/BomPluginIntegrationTests.java b/buildSrc/src/test/java/org/springframework/boot/build/bom/BomPluginIntegrationTests.java
new file mode 100644
index 0000000000..3bd1c2bb4a
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/bom/BomPluginIntegrationTests.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2012-2020 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.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.util.function.Consumer;
+
+import org.gradle.testkit.runner.BuildResult;
+import org.gradle.testkit.runner.GradleRunner;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import org.springframework.boot.build.DeployedPlugin;
+import org.springframework.boot.build.assertj.NodeAssert;
+import org.springframework.util.FileCopyUtils;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link BomPlugin}.
+ *
+ * @author Andy Wilkinson
+ */
+public class BomPluginIntegrationTests {
+
+ private File projectDir;
+
+ private File buildFile;
+
+ @BeforeEach
+ public void setup(@TempDir File projectDir) throws IOException {
+ this.projectDir = projectDir;
+ this.buildFile = new File(this.projectDir, "build.gradle");
+ }
+
+ @Test
+ void libraryModulesAreIncludedInDependencyManagementOfGeneratedPom() throws IOException {
+ try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
+ out.println("plugins {");
+ out.println(" id 'org.springframework.boot.bom'");
+ out.println("}");
+ out.println("bom {");
+ out.println(" library('ActiveMQ', '5.15.10') {");
+ out.println(" group('org.apache.activemq') {");
+ out.println(" modules = [");
+ out.println(" 'activemq-amqp',");
+ out.println(" 'activemq-blueprint'");
+ out.println(" ]");
+ out.println(" }");
+ out.println(" }");
+ out.println("}");
+ }
+ generatePom((pom) -> {
+ assertThat(pom).textAtPath("//properties/activemq.version").isEqualTo("5.15.10");
+ NodeAssert dependency = pom.nodeAtPath("//dependencyManagement/dependencies/dependency[1]");
+ assertThat(dependency).textAtPath("groupId").isEqualTo("org.apache.activemq");
+ assertThat(dependency).textAtPath("artifactId").isEqualTo("activemq-amqp");
+ assertThat(dependency).textAtPath("version").isEqualTo("${activemq.version}");
+ assertThat(dependency).textAtPath("scope").isNullOrEmpty();
+ assertThat(dependency).textAtPath("type").isNullOrEmpty();
+ dependency = pom.nodeAtPath("//dependencyManagement/dependencies/dependency[2]");
+ assertThat(dependency).textAtPath("groupId").isEqualTo("org.apache.activemq");
+ assertThat(dependency).textAtPath("artifactId").isEqualTo("activemq-blueprint");
+ assertThat(dependency).textAtPath("version").isEqualTo("${activemq.version}");
+ assertThat(dependency).textAtPath("scope").isNullOrEmpty();
+ assertThat(dependency).textAtPath("type").isNullOrEmpty();
+ });
+ }
+
+ @Test
+ void libraryPluginsAreIncludedInPluginManagementOfGeneratedPom() throws IOException {
+ try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
+ out.println("plugins {");
+ out.println(" id 'org.springframework.boot.bom'");
+ out.println("}");
+ out.println("bom {");
+ out.println(" library('Flyway', '6.0.8') {");
+ out.println(" group('org.flywaydb') {");
+ out.println(" plugins = [");
+ out.println(" 'flyway-maven-plugin'");
+ out.println(" ]");
+ out.println(" }");
+ out.println(" }");
+ out.println("}");
+ }
+ generatePom((pom) -> {
+ assertThat(pom).textAtPath("//properties/flyway.version").isEqualTo("6.0.8");
+ NodeAssert plugin = pom.nodeAtPath("//pluginManagement/plugins/plugin");
+ assertThat(plugin).textAtPath("groupId").isEqualTo("org.flywaydb");
+ assertThat(plugin).textAtPath("artifactId").isEqualTo("flyway-maven-plugin");
+ assertThat(plugin).textAtPath("version").isEqualTo("${flyway.version}");
+ assertThat(plugin).textAtPath("scope").isNullOrEmpty();
+ assertThat(plugin).textAtPath("type").isNullOrEmpty();
+ });
+ }
+
+ @Test
+ void libraryImportsAreIncludedInDependencyManagementOfGeneratedPom() throws Exception {
+ try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
+ out.println("plugins {");
+ out.println(" id 'org.springframework.boot.bom'");
+ out.println("}");
+ out.println("bom {");
+ out.println(" library('Jackson Bom', '2.10.0') {");
+ out.println(" group('com.fasterxml.jackson') {");
+ out.println(" imports = [");
+ out.println(" 'jackson-bom'");
+ out.println(" ]");
+ out.println(" }");
+ out.println(" }");
+ out.println("}");
+ }
+ generatePom((pom) -> {
+ assertThat(pom).textAtPath("//properties/jackson-bom.version").isEqualTo("2.10.0");
+ NodeAssert dependency = pom.nodeAtPath("//dependencyManagement/dependencies/dependency");
+ assertThat(dependency).textAtPath("groupId").isEqualTo("com.fasterxml.jackson");
+ assertThat(dependency).textAtPath("artifactId").isEqualTo("jackson-bom");
+ assertThat(dependency).textAtPath("version").isEqualTo("${jackson-bom.version}");
+ assertThat(dependency).textAtPath("scope").isEqualTo("import");
+ assertThat(dependency).textAtPath("type").isEqualTo("pom");
+ });
+ }
+
+ @Test
+ void moduleExclusionsAreIncludedInDependencyManagementOfGeneratedPom() throws IOException {
+ try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
+ out.println("plugins {");
+ out.println(" id 'org.springframework.boot.bom'");
+ out.println("}");
+ out.println("bom {");
+ out.println(" library('MySQL', '8.0.18') {");
+ out.println(" group('mysql') {");
+ out.println(" modules = [");
+ out.println(" 'mysql-connector-java' {");
+ out.println(" exclude group: 'com.google.protobuf', module: 'protobuf-java'");
+ out.println(" }");
+ out.println(" ]");
+ out.println(" }");
+ out.println(" }");
+ out.println("}");
+ }
+ generatePom((pom) -> {
+ assertThat(pom).textAtPath("//properties/mysql.version").isEqualTo("8.0.18");
+ NodeAssert dependency = pom.nodeAtPath("//dependencyManagement/dependencies/dependency");
+ assertThat(dependency).textAtPath("groupId").isEqualTo("mysql");
+ assertThat(dependency).textAtPath("artifactId").isEqualTo("mysql-connector-java");
+ assertThat(dependency).textAtPath("version").isEqualTo("${mysql.version}");
+ assertThat(dependency).textAtPath("scope").isNullOrEmpty();
+ assertThat(dependency).textAtPath("type").isNullOrEmpty();
+ NodeAssert exclusion = dependency.nodeAtPath("exclusions/exclusion");
+ assertThat(exclusion).textAtPath("groupId").isEqualTo("com.google.protobuf");
+ assertThat(exclusion).textAtPath("artifactId").isEqualTo("protobuf-java");
+ });
+ }
+
+ private BuildResult runGradle(String... args) {
+ return GradleRunner.create().withDebug(true).withProjectDir(this.projectDir).withArguments(args)
+ .withPluginClasspath().build();
+ }
+
+ private void generatePom(Consumer consumer) {
+ runGradle(DeployedPlugin.GENERATE_POM_TASK_NAME, "-s");
+ File generatedPomXml = new File(this.projectDir, "build/publications/maven/pom-default.xml");
+ try (Reader reader = new FileReader(generatedPomXml)) {
+ System.out.println(FileCopyUtils.copyToString(reader));
+ }
+ catch (IOException ex) {
+
+ }
+ assertThat(generatedPomXml).isFile();
+ consumer.accept(new NodeAssert(generatedPomXml));
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/ArtifactVersionDependencyVersionTests.java b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/ArtifactVersionDependencyVersionTests.java
new file mode 100644
index 0000000000..643baa2219
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/ArtifactVersionDependencyVersionTests.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ArtifactVersionDependencyVersion}.
+ *
+ * @author Andy Wilkinson
+ */
+public class ArtifactVersionDependencyVersionTests {
+
+ @Test
+ void parseWhenVersionIsNotAMavenVersionShouldReturnNull() {
+ assertThat(version("1.2.3.1")).isNull();
+ }
+
+ @Test
+ void parseWhenVersionIsAMavenVersionShouldReturnAVersion() {
+ assertThat(version("1.2.3")).isNotNull();
+ }
+
+ @Test
+ void isNewerThanWhenInputIsOlderMajorShouldReturnTrue() {
+ assertThat(version("2.1.2").isNewerThan(version("1.9.0"))).isTrue();
+ }
+
+ @Test
+ void isNewerThanWhenInputIsOlderMinorShouldReturnTrue() {
+ assertThat(version("2.1.2").isNewerThan(version("2.0.2"))).isTrue();
+ }
+
+ @Test
+ void isNewerThanWhenInputIsOlderPatchShouldReturnTrue() {
+ assertThat(version("2.1.2").isNewerThan(version("2.1.1"))).isTrue();
+ }
+
+ @Test
+ void isNewerThanWhenInputIsNewerMajorShouldReturnFalse() {
+ assertThat(version("2.1.2").isNewerThan(version("3.2.1"))).isFalse();
+ }
+
+ @Test
+ void isSameMajorAndNewerThanWhenMinorIsOlderShouldReturnTrue() {
+ assertThat(version("1.10.2").isSameMajorAndNewerThan(version("1.9.0"))).isTrue();
+ }
+
+ @Test
+ void isSameMajorAndNewerThanWhenMajorIsOlderShouldReturnFalse() {
+ assertThat(version("2.0.2").isSameMajorAndNewerThan(version("1.9.0"))).isFalse();
+ }
+
+ @Test
+ void isSameMajorAndNewerThanWhenPatchIsNewerShouldReturnTrue() {
+ assertThat(version("2.1.2").isSameMajorAndNewerThan(version("2.1.1"))).isTrue();
+ }
+
+ @Test
+ void isSameMajorAndNewerThanWhenMinorIsNewerShouldReturnFalse() {
+ assertThat(version("2.1.2").isSameMajorAndNewerThan(version("2.2.1"))).isFalse();
+ }
+
+ @Test
+ void isSameMajorAndNewerThanWhenMajorIsNewerShouldReturnFalse() {
+ assertThat(version("2.1.2").isSameMajorAndNewerThan(version("3.0.1"))).isFalse();
+ }
+
+ @Test
+ void isSameMinorAndNewerThanWhenPatchIsOlderShouldReturnTrue() {
+ assertThat(version("1.10.2").isSameMinorAndNewerThan(version("1.10.1"))).isTrue();
+ }
+
+ @Test
+ void isSameMinorAndNewerThanWhenMinorIsOlderShouldReturnFalse() {
+ assertThat(version("2.1.2").isSameMinorAndNewerThan(version("2.0.1"))).isFalse();
+ }
+
+ @Test
+ void isSameMinorAndNewerThanWhenVersionsAreTheSameShouldReturnFalse() {
+ assertThat(version("2.1.2").isSameMinorAndNewerThan(version("2.1.2"))).isFalse();
+ }
+
+ @Test
+ void isSameMinorAndNewerThanWhenPatchIsNewerShouldReturnFalse() {
+ assertThat(version("2.1.2").isSameMinorAndNewerThan(version("2.1.3"))).isFalse();
+ }
+
+ @Test
+ void isSameMinorAndNewerThanWhenMinorIsNewerShouldReturnFalse() {
+ assertThat(version("2.1.2").isSameMinorAndNewerThan(version("2.0.1"))).isFalse();
+ }
+
+ @Test
+ void isSameMinorAndNewerThanWhenMajorIsNewerShouldReturnFalse() {
+ assertThat(version("3.1.2").isSameMinorAndNewerThan(version("2.0.1"))).isFalse();
+ }
+
+ private ArtifactVersionDependencyVersion version(String version) {
+ return ArtifactVersionDependencyVersion.parse(version);
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/DependencyVersionTests.java b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/DependencyVersionTests.java
new file mode 100644
index 0000000000..2e0b16d7f0
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/DependencyVersionTests.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link DependencyVersion}.
+ *
+ * @author Andy Wilkinson
+ */
+public class DependencyVersionTests {
+
+ @Test
+ void parseWhenValidMavenVersionShouldReturnArtifactVersionDependencyVersion() {
+ assertThat(DependencyVersion.parse("1.2.3.Final")).isInstanceOf(ArtifactVersionDependencyVersion.class);
+ }
+
+ @Test
+ void parseWhenReleaseTrainShouldReturnReleaseTrainDependencyVersion() {
+ assertThat(DependencyVersion.parse("Ingalls-SR5")).isInstanceOf(ReleaseTrainDependencyVersion.class);
+ }
+
+ @Test
+ void parseWhenMavenLikeVersionWithNumericQualifieShouldReturnNumericQualifierDependencyVersion() {
+ assertThat(DependencyVersion.parse("1.2.3.4")).isInstanceOf(NumericQualifierDependencyVersion.class);
+ }
+
+ @Test
+ void parseWhenVersionWithLeadingZeroesShouldReturnLeadingZeroesDependencyVersion() {
+ assertThat(DependencyVersion.parse("1.4.01")).isInstanceOf(LeadingZeroesDependencyVersion.class);
+ }
+
+ @Test
+ void parseWhenVersionWithCombinedPatchAndQualifierShouldReturnCombinedPatchAndQualifierDependencyVersion() {
+ assertThat(DependencyVersion.parse("4.0.0M4")).isInstanceOf(CombinedPatchAndQualifierDependencyVersion.class);
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/NumericQualifierDependencyVersionTests.java b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/NumericQualifierDependencyVersionTests.java
new file mode 100644
index 0000000000..5e58d3a99c
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/NumericQualifierDependencyVersionTests.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link NumericQualifierDependencyVersion}.
+ *
+ * @author Andy Wilkinson
+ */
+public class NumericQualifierDependencyVersionTests {
+
+ @Test
+ void isNewerThanOnVersionWithNumericQualifierWhenInputHasNoQualifierShouldReturnTrue() {
+ assertThat(version("2.9.9.20190806").isNewerThan(DependencyVersion.parse("2.9.9"))).isTrue();
+ }
+
+ @Test
+ void isNewerThanOnVersionWithNumericQualifierWhenInputHasOlderQualifierShouldReturnTrue() {
+ assertThat(version("2.9.9.20190806").isNewerThan(version("2.9.9.20190805"))).isTrue();
+ }
+
+ @Test
+ void isNewerThanOnVersionWithNumericQualifierWhenInputHasNewerQualifierShouldReturnFalse() {
+ assertThat(version("2.9.9.20190806").isNewerThan(version("2.9.9.20190807"))).isFalse();
+ }
+
+ @Test
+ void isNewerThanOnVersionWithNumericQualifierWhenInputHasSameQualifierShouldReturnFalse() {
+ assertThat(version("2.9.9.20190806").isNewerThan(version("2.9.9.20190806"))).isFalse();
+ }
+
+ private NumericQualifierDependencyVersion version(String version) {
+ return NumericQualifierDependencyVersion.parse(version);
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersionTests.java b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersionTests.java
new file mode 100644
index 0000000000..6010bfd075
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/bom/bomr/version/ReleaseTrainDependencyVersionTests.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2012-2020 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.bomr.version;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ReleaseTrainDependencyVersion}.
+ *
+ * @author Andy Wilkinson
+ */
+public class ReleaseTrainDependencyVersionTests {
+
+ @Test
+ void parsingOfANonReleaseTrainVersionReturnsNull() {
+ assertThat(version("5.1.4.RELEASE")).isNull();
+ }
+
+ @Test
+ void parsingOfAReleaseTrainVersionReturnsVersion() {
+ assertThat(version("Lovelace-SR3")).isNotNull();
+ }
+
+ @Test
+ void isNewerThanWhenReleaseTrainIsNewerShouldReturnTrue() {
+ assertThat(version("Lovelace-RELEASE").isNewerThan(version("Kay-SR5"))).isTrue();
+ }
+
+ @Test
+ void isNewerThanWhenVersionIsNewerShouldReturnTrue() {
+ assertThat(version("Kay-SR10").isNewerThan(version("Kay-SR5"))).isTrue();
+ }
+
+ @Test
+ void isNewerThanWhenVersionIsOlderShouldReturnFalse() {
+ assertThat(version("Kay-RELEASE").isNewerThan(version("Kay-SR5"))).isFalse();
+ }
+
+ @Test
+ void isNewerThanWhenReleaseTrainIsOlderShouldReturnFalse() {
+ assertThat(version("Ingalls-RELEASE").isNewerThan(version("Kay-SR5"))).isFalse();
+ }
+
+ @Test
+ void isSameMajorAndNewerWhenWhenReleaseTrainIsNewerShouldReturnTrue() {
+ assertThat(version("Lovelace-RELEASE").isSameMajorAndNewerThan(version("Kay-SR5"))).isTrue();
+ }
+
+ @Test
+ void isSameMajorAndNewerThanWhenReleaseTrainIsOlderShouldReturnFalse() {
+ assertThat(version("Ingalls-RELEASE").isSameMajorAndNewerThan(version("Kay-SR5"))).isFalse();
+ }
+
+ @Test
+ void isSameMajorAndNewerThanWhenVersionIsNewerShouldReturnTrue() {
+ assertThat(version("Kay-SR6").isSameMajorAndNewerThan(version("Kay-SR5"))).isTrue();
+ }
+
+ @Test
+ void isSameMinorAndNewerThanWhenReleaseTrainIsNewerShouldReturnFalse() {
+ assertThat(version("Lovelace-RELEASE").isSameMinorAndNewerThan(version("Kay-SR5"))).isFalse();
+ }
+
+ @Test
+ void isSameMinorAndNewerThanWhenReleaseTrainIsTheSameAndVersionIsNewerShouldReturnTrue() {
+ assertThat(version("Kay-SR6").isSameMinorAndNewerThan(version("Kay-SR5"))).isTrue();
+ }
+
+ @Test
+ void isSameMinorAndNewerThanWhenReleaseTrainAndVersionAreTheSameShouldReturnFalse() {
+ assertThat(version("Kay-SR6").isSameMinorAndNewerThan(version("Kay-SR6"))).isFalse();
+ }
+
+ @Test
+ void isSameMinorAndNewerThanWhenReleaseTrainIsTheSameAndVersionIsOlderShouldReturnFalse() {
+ assertThat(version("Kay-SR6").isSameMinorAndNewerThan(version("Kay-SR7"))).isFalse();
+ }
+
+ private static ReleaseTrainDependencyVersion version(String input) {
+ return ReleaseTrainDependencyVersion.parse(input);
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/context/properties/CompoundConfigurationTableEntryTests.java b/buildSrc/src/test/java/org/springframework/boot/build/context/properties/CompoundConfigurationTableEntryTests.java
new file mode 100644
index 0000000000..2f229b93b7
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/context/properties/CompoundConfigurationTableEntryTests.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link CompoundConfigurationTableEntry}.
+ *
+ * @author Brian Clozel
+ */
+public class CompoundConfigurationTableEntryTests {
+
+ private static String NEWLINE = System.lineSeparator();
+
+ @Test
+ void simpleProperty() {
+ ConfigurationProperty firstProp = new ConfigurationProperty("spring.test.first", "java.lang.String");
+ ConfigurationProperty secondProp = new ConfigurationProperty("spring.test.second", "java.lang.String");
+ ConfigurationProperty thirdProp = new ConfigurationProperty("spring.test.third", "java.lang.String");
+ CompoundConfigurationTableEntry entry = new CompoundConfigurationTableEntry("spring.test",
+ "This is a description.");
+ entry.addConfigurationKeys(firstProp, secondProp, thirdProp);
+ AsciidocBuilder builder = new AsciidocBuilder();
+ entry.write(builder);
+ assertThat(builder.toString()).isEqualTo(
+ "|`+spring.test.first+` +" + NEWLINE + "`+spring.test.second+` +" + NEWLINE + "`+spring.test.third+` +"
+ + NEWLINE + NEWLINE + "|" + NEWLINE + "|+++This is a description.+++" + NEWLINE);
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationTableTests.java b/buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationTableTests.java
new file mode 100644
index 0000000000..c757c73f16
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/context/properties/ConfigurationTableTests.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ConfigurationTable}.
+ *
+ * @author Brian Clozel
+ */
+public class ConfigurationTableTests {
+
+ private static String NEWLINE = System.lineSeparator();
+
+ @Test
+ void simpleTable() {
+ ConfigurationTable table = new ConfigurationTable("test");
+ ConfigurationProperty first = new ConfigurationProperty("spring.test.prop", "java.lang.String", "something",
+ "This is a description.", false);
+ ConfigurationProperty second = new ConfigurationProperty("spring.test.other", "java.lang.String", "other value",
+ "This is another description.", false);
+ table.addEntry(new SingleConfigurationTableEntry(first));
+ table.addEntry(new SingleConfigurationTableEntry(second));
+ assertThat(table.toAsciidocTable()).isEqualTo("[cols=\"1,1,2\", options=\"header\"]" + NEWLINE + "|==="
+ + NEWLINE + "|Key|Default Value|Description" + NEWLINE + NEWLINE + "|`+spring.test.other+`" + NEWLINE
+ + "|`+other value+`" + NEWLINE + "|+++This is another description.+++" + NEWLINE + NEWLINE
+ + "|`+spring.test.prop+`" + NEWLINE + "|`+something+`" + NEWLINE + "|+++This is a description.+++"
+ + NEWLINE + NEWLINE + "|===" + NEWLINE);
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/context/properties/SingleConfigurationTableEntryTests.java b/buildSrc/src/test/java/org/springframework/boot/build/context/properties/SingleConfigurationTableEntryTests.java
new file mode 100644
index 0000000000..615cdca6ae
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/context/properties/SingleConfigurationTableEntryTests.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2012-2020 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.context.properties;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link SingleConfigurationTableEntry}.
+ *
+ * @author Brian Clozel
+ */
+public class SingleConfigurationTableEntryTests {
+
+ private static String NEWLINE = System.lineSeparator();
+
+ @Test
+ void simpleProperty() {
+ ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", "java.lang.String", "something",
+ "This is a description.", false);
+ SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property);
+ AsciidocBuilder builder = new AsciidocBuilder();
+ entry.write(builder);
+ assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|`+something+`" + NEWLINE
+ + "|+++This is a description.+++" + NEWLINE);
+ }
+
+ @Test
+ void noDefaultValue() {
+ ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", "java.lang.String", null,
+ "This is a description.", false);
+ SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property);
+ AsciidocBuilder builder = new AsciidocBuilder();
+ entry.write(builder);
+ assertThat(builder.toString()).isEqualTo(
+ "|`+spring.test.prop+`" + NEWLINE + "|" + NEWLINE + "|+++This is a description.+++" + NEWLINE);
+ }
+
+ @Test
+ void defaultValueWithPipes() {
+ ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", "java.lang.String",
+ "first|second", "This is a description.", false);
+ SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property);
+ AsciidocBuilder builder = new AsciidocBuilder();
+ entry.write(builder);
+ assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|`+first\\|second+`" + NEWLINE
+ + "|+++This is a description.+++" + NEWLINE);
+ }
+
+ @Test
+ void defaultValueWithBackslash() {
+ ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", "java.lang.String",
+ "first\\second", "This is a description.", false);
+ SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property);
+ AsciidocBuilder builder = new AsciidocBuilder();
+ entry.write(builder);
+ assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|`+first\\\\second+`" + NEWLINE
+ + "|+++This is a description.+++" + NEWLINE);
+ }
+
+ @Test
+ void descriptionWithPipe() {
+ ConfigurationProperty property = new ConfigurationProperty("spring.test.prop", "java.lang.String", null,
+ "This is a description with a | pipe.", false);
+ SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property);
+ AsciidocBuilder builder = new AsciidocBuilder();
+ entry.write(builder);
+ assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|" + NEWLINE
+ + "|+++This is a description with a \\| pipe.+++" + NEWLINE);
+ }
+
+ @Test
+ void mapProperty() {
+ ConfigurationProperty property = new ConfigurationProperty("spring.test.prop",
+ "java.util.Map", null, "This is a description.", false);
+ SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property);
+ AsciidocBuilder builder = new AsciidocBuilder();
+ entry.write(builder);
+ assertThat(builder.toString()).isEqualTo(
+ "|`+spring.test.prop.*+`" + NEWLINE + "|" + NEWLINE + "|+++This is a description.+++" + NEWLINE);
+ }
+
+ @Test
+ void listProperty() {
+ String[] defaultValue = new String[] { "first", "second", "third" };
+ ConfigurationProperty property = new ConfigurationProperty("spring.test.prop",
+ "java.util.List", defaultValue, "This is a description.", false);
+ SingleConfigurationTableEntry entry = new SingleConfigurationTableEntry(property);
+ AsciidocBuilder builder = new AsciidocBuilder();
+ entry.write(builder);
+ assertThat(builder.toString()).isEqualTo("|`+spring.test.prop+`" + NEWLINE + "|`+first," + NEWLINE + "second,"
+ + NEWLINE + "third+`" + NEWLINE + "|+++This is a description.+++" + NEWLINE);
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/log4j2/ReproduciblePluginsDatActionTests.java b/buildSrc/src/test/java/org/springframework/boot/build/log4j2/ReproduciblePluginsDatActionTests.java
new file mode 100644
index 0000000000..f482f5353e
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/log4j2/ReproduciblePluginsDatActionTests.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2019 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.log4j2;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import org.apache.logging.log4j.core.config.plugins.processor.PluginCache;
+import org.apache.logging.log4j.core.config.plugins.processor.PluginEntry;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ReproducibleLog4j2PluginsDatAction}
+ *
+ * @author Andy Wilkinson
+ */
+public class ReproduciblePluginsDatActionTests {
+
+ @Test
+ void postProcessingOrdersCategoriesAndPlugins() throws IOException {
+ Path datFile = Files.createTempFile("Log4j2Plugins", "dat");
+ try {
+ write(datFile);
+ PluginCache cache = new PluginCache();
+ cache.loadCacheFiles(new Vector<>(Arrays.asList(datFile.toUri().toURL())).elements());
+ assertThat(cache.getAllCategories().keySet()).containsExactly("one", "two");
+ assertThat(cache.getCategory("one").keySet()).containsExactly("alpha", "bravo", "charlie");
+ assertThat(cache.getCategory("two").keySet()).containsExactly("delta", "echo", "foxtrot");
+ }
+ finally {
+ Files.delete(datFile);
+ }
+ }
+
+ private void write(Path datFile) throws IOException {
+ PluginCache cache = new PluginCache();
+ createCategory(cache, "two", Arrays.asList("delta", "foxtrot", "echo"));
+ createCategory(cache, "one", Arrays.asList("bravo", "alpha", "charlie"));
+ try (OutputStream output = new FileOutputStream(datFile.toFile())) {
+ cache.writeCache(output);
+ new ReproducibleLog4j2PluginsDatAction().postProcess(datFile.toFile());
+ }
+ }
+
+ private void createCategory(PluginCache cache, String categoryName, List entryNames) {
+ Map category = cache.getCategory(categoryName);
+ for (String entryName : entryNames) {
+ PluginEntry entry = new PluginEntry();
+ entry.setKey(entryName);
+ entry.setClassName("com.example.Plugin");
+ entry.setName("name");
+ entry.setCategory(categoryName);
+ category.put(entryName, entry);
+ }
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/mavenplugin/PluginXmlParserTests.java b/buildSrc/src/test/java/org/springframework/boot/build/mavenplugin/PluginXmlParserTests.java
new file mode 100644
index 0000000000..b5a4f83820
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/mavenplugin/PluginXmlParserTests.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2019 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.mavenplugin;
+
+import java.io.File;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.build.mavenplugin.PluginXmlParser.Plugin;
+
+/**
+ * Tests for {@link PluginXmlParser}.
+ *
+ * @author Andy Wilkinson
+ */
+public class PluginXmlParserTests {
+
+ private final PluginXmlParser parser = new PluginXmlParser();
+
+ @Test
+ void dunno() {
+ Plugin plugin = this.parser.parse(new File("src/test/resources/plugin.xml"));
+ System.out.println(plugin);
+ }
+
+}
diff --git a/buildSrc/src/test/java/org/springframework/boot/build/optional/OptionalDependenciesPluginIntegrationTests.java b/buildSrc/src/test/java/org/springframework/boot/build/optional/OptionalDependenciesPluginIntegrationTests.java
new file mode 100644
index 0000000000..d5585ee9e6
--- /dev/null
+++ b/buildSrc/src/test/java/org/springframework/boot/build/optional/OptionalDependenciesPluginIntegrationTests.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2012-2020 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.optional;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import org.gradle.testkit.runner.BuildResult;
+import org.gradle.testkit.runner.GradleRunner;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for {@link OptionalDependenciesPlugin}.
+ *
+ * @author Andy Wilkinson
+ */
+public class OptionalDependenciesPluginIntegrationTests {
+
+ private File projectDir;
+
+ private File buildFile;
+
+ @BeforeEach
+ public void setup(@TempDir File projectDir) throws IOException {
+ this.projectDir = projectDir;
+ this.buildFile = new File(this.projectDir, "build.gradle");
+ }
+
+ @Test
+ void optionalConfigurationIsCreated() throws IOException {
+ try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
+ out.println("plugins { id 'org.springframework.boot.optional-dependencies' }");
+ out.println("task printConfigurations {");
+ out.println(" doLast {");
+ out.println(" configurations.all { println it.name }");
+ out.println(" }");
+ out.println("}");
+ }
+ BuildResult buildResult = runGradle("printConfigurations");
+ assertThat(buildResult.getOutput()).contains("optional");
+ }
+
+ @Test
+ void optionalDependenciesAreAddedToMainSourceSetsCompileClasspath() throws IOException {
+ optionalDependenciesAreAddedToSourceSetClasspath("main", "compileClasspath");
+ }
+
+ @Test
+ void optionalDependenciesAreAddedToMainSourceSetsRuntimeClasspath() throws IOException {
+ optionalDependenciesAreAddedToSourceSetClasspath("main", "runtimeClasspath");
+ }
+
+ @Test
+ void optionalDependenciesAreAddedToTestSourceSetsCompileClasspath() throws IOException {
+ optionalDependenciesAreAddedToSourceSetClasspath("test", "compileClasspath");
+ }
+
+ @Test
+ void optionalDependenciesAreAddedToTestSourceSetsRuntimeClasspath() throws IOException {
+ optionalDependenciesAreAddedToSourceSetClasspath("test", "runtimeClasspath");
+ }
+
+ public void optionalDependenciesAreAddedToSourceSetClasspath(String sourceSet, String classpath)
+ throws IOException {
+ try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
+ out.println("plugins {");
+ out.println(" id 'org.springframework.boot.optional-dependencies'");
+ out.println(" id 'java'");
+ out.println("}");
+ out.println("repositories {");
+ out.println(" mavenCentral()");
+ out.println("}");
+ out.println("dependencies {");
+ out.println(" optional 'org.springframework:spring-jcl:5.1.2.RELEASE'");
+ out.println("}");
+ out.println("task printClasspath {");
+ out.println(" doLast {");
+ out.println(" println sourceSets." + sourceSet + "." + classpath + ".files");
+ out.println(" }");
+ out.println("}");
+ }
+ BuildResult buildResult = runGradle("printClasspath");
+ assertThat(buildResult.getOutput()).contains("spring-jcl");
+ }
+
+ private BuildResult runGradle(String... args) {
+ return GradleRunner.create().withProjectDir(this.projectDir).withArguments(args).withPluginClasspath().build();
+ }
+
+}
diff --git a/buildSrc/src/test/resources/plugin.xml b/buildSrc/src/test/resources/plugin.xml
new file mode 100644
index 0000000000..f9464b5a5d
--- /dev/null
+++ b/buildSrc/src/test/resources/plugin.xml
@@ -0,0 +1,911 @@
+
+
+
+
+
+ Spring Boot Maven Plugin
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+ 2.2.0.GRADLE-SNAPSHOT
+ spring-boot
+ false
+ true
+
+
+ build-info
+ Generate a {@code build-info.properties} file based the content of the current
+{@link MavenProject}.
+ false
+ true
+ false
+ false
+ false
+ true
+ generate-resources
+ org.springframework.boot.maven.BuildInfoMojo
+ java
+ per-lookup
+ once-per-session
+ 1.4.0
+ true
+
+
+ additionalProperties
+ java.util.Map
+ false
+ true
+ Additional properties to store in the build-info.properties. Each entry is prefixed
+by {@code build.} in the generated build-info.properties.
+
+
+ outputFile
+ java.io.File
+ false
+ true
+ The location of the generated build-info.properties.
+
+
+ project
+ org.apache.maven.project.MavenProject
+ true
+ false
+ The Maven project.
+
+
+ session
+ org.apache.maven.execution.MavenSession
+ true
+ false
+ The Maven session.
+
+
+ time
+ java.lang.String
+ 2.2.0
+ false
+ true
+ The value used for the {@code build.time} property in a form suitable for
+{@link Instant#parse(CharSequence)}. Defaults to {@code session.request.startTime}.
+To disable the {@code build.time} property entirely, use {@code 'off'}.
+
+
+
+
+
+
+
+
+
+ org.sonatype.plexus.build.incremental.BuildContext
+ buildContext
+
+
+
+
+ help
+ Display help information on spring-boot-maven-plugin.<br>
+Call <code>mvn spring-boot:help -Ddetail=true -Dgoal=<goal-name></code> to display parameter details.
+ false
+ false
+ false
+ false
+ false
+ true
+ org.springframework.boot.maven.HelpMojo
+ java
+ per-lookup
+ once-per-session
+ true
+
+
+ detail
+ boolean
+ false
+ true
+ If <code>true</code>, display all settable properties for each goal.
+
+
+ goal
+ java.lang.String
+ false
+ true
+ The name of the goal for which to show help. If unspecified, all goals will be displayed.
+
+
+ indentSize
+ int
+ false
+ true
+ The number of spaces per indentation level, should be positive.
+
+
+ lineLength
+ int
+ false
+ true
+ The maximum length of a display line, should be positive.
+
+
+
+ ${detail}
+ ${goal}
+ ${indentSize}
+ ${lineLength}
+
+
+
+ repackage
+ Repackages existing JAR and WAR archives so that they can be executed from the command
+line using {@literal java -jar}. With <code>layout=NONE</code> can also be used simply
+to package a JAR with nested dependencies (and no main class, so not executable).
+ compile+runtime
+ false
+ true
+ false
+ false
+ false
+ true
+ package
+ org.springframework.boot.maven.RepackageMojo
+ java
+ per-lookup
+ once-per-session
+ 1.0.0
+ compile+runtime
+ true
+
+
+ attach
+ boolean
+ 1.4.0
+ false
+ true
+ Attach the repackaged archive to be installed and deployed.
+
+
+ classifier
+ java.lang.String
+ 1.0.0
+ false
+ true
+ Classifier to add to the repackaged archive. If not given, the main artifact will
+be replaced by the repackaged archive. If given, the classifier will also be used
+to determine the source archive to repackage: if an artifact with that classifier
+already exists, it will be used as source and replaced. If no such artifact exists,
+the main artifact will be used as source and the repackaged archive will be
+attached as a supplemental artifact with that classifier. Attaching the artifact
+allows to deploy it alongside to the original one, see <a href=
+"https://maven.apache.org/plugins/maven-deploy-plugin/examples/deploying-with-classifiers.html"
+>the Maven documentation for more details</a>.
+
+
+ embeddedLaunchScript
+ java.io.File
+ 1.3.0
+ false
+ true
+ The embedded launch script to prepend to the front of the jar if it is fully
+executable. If not specified the 'Spring Boot' default script will be used.
+
+
+ embeddedLaunchScriptProperties
+ java.util.Properties
+ 1.3.0
+ false
+ true
+ Properties that should be expanded in the embedded launch script.
+
+
+ excludeDevtools
+ boolean
+ 1.3.0
+ false
+ true
+ Exclude Spring Boot devtools from the repackaged archive.
+
+
+ excludeGroupIds
+ java.lang.String
+ 1.1.0
+ false
+ true
+ Comma separated list of groupId names to exclude (exact match).
+
+
+ excludes
+ java.util.List
+ 1.1.0
+ false
+ true
+ Collection of artifact definitions to exclude. The {@link Exclude} element defines
+a {@code groupId} and {@code artifactId} mandatory properties and an optional
+{@code classifier} property.
+
+
+ executable
+ boolean
+ 1.3.0
+ false
+ true
+ Make a fully executable jar for *nix machines by prepending a launch script to the
+jar.
+<p>
+Currently, some tools do not accept this format so you may not always be able to
+use this technique. For example, {@code jar -xf} may silently fail to extract a jar
+or war that has been made fully-executable. It is recommended that you only enable
+this option if you intend to execute it directly, rather than running it with
+{@code java -jar} or deploying it to a servlet container.
+
+
+ finalName
+ java.lang.String
+ 1.0.0
+ false
+ false
+ Name of the generated archive.
+
+
+ includeSystemScope
+ boolean
+ 1.4.0
+ false
+ true
+ Include system scoped dependencies.
+
+
+ includes
+ java.util.List
+ 1.2.0
+ false
+ true
+ Collection of artifact definitions to include. The {@link Include} element defines
+a {@code groupId} and {@code artifactId} mandatory properties and an optional
+{@code classifier} property.
+
+
+ layout
+ org.springframework.boot.maven.RepackageMojo$LayoutType
+ 1.0.0
+ false
+ true
+ The type of archive (which corresponds to how the dependencies are laid out inside
+it). Possible values are JAR, WAR, ZIP, DIR, NONE. Defaults to a guess based on the
+archive type.
+
+
+ layoutFactory
+ org.springframework.boot.loader.tools.LayoutFactory
+ 1.5.0
+ false
+ true
+ The layout factory that will be used to create the executable archive if no
+explicit layout is set. Alternative layouts implementations can be provided by 3rd
+parties.
+
+
+ mainClass
+ java.lang.String
+ 1.0.0
+ false
+ true
+ The name of the main class. If not specified the first compiled class found that
+contains a 'main' method will be used.
+
+
+ outputDirectory
+ java.io.File
+ 1.0.0
+ true
+ true
+ Directory containing the generated archive.
+
+
+ project
+ org.apache.maven.project.MavenProject
+ 1.0.0
+ true
+ false
+ The Maven project.
+
+
+ requiresUnpack
+ java.util.List
+ 1.1.0
+ false
+ true
+ A list of the libraries that must be unpacked from fat jars in order to run.
+Specify each library as a {@code <dependency>} with a {@code <groupId>} and a
+{@code <artifactId>} and they will be unpacked at runtime.
+
+
+ skip
+ boolean
+ 1.2.0
+ false
+ true
+ Skip the execution.
+
+
+
+
+ ${spring-boot.repackage.excludeDevtools}
+ ${spring-boot.excludeGroupIds}
+ ${spring-boot.excludes}
+
+
+
+ ${spring-boot.includes}
+ ${spring-boot.repackage.layout}
+
+
+ ${spring-boot.repackage.skip}
+
+
+
+ org.apache.maven.project.MavenProjectHelper
+ projectHelper
+
+
+
+
+ run
+ Run an executable archive application.
+ test
+ false
+ true
+ false
+ false
+ false
+ true
+ validate
+ test-compile
+ org.springframework.boot.maven.RunMojo
+ java
+ per-lookup
+ once-per-session
+ 1.0.0
+ false
+
+
+ addResources
+ boolean
+ 1.0.0
+ false
+ true
+ Add maven resources to the classpath directly, this allows live in-place editing of
+resources. Duplicate resources are removed from {@code target/classes} to prevent
+them to appear twice if {@code ClassLoader.getResources()} is called. Please
+consider adding {@code spring-boot-devtools} to your project instead as it provides
+this feature and many more.
+
+
+ agent
+ java.io.File[]
+ 1.0.0
+ since 2.2.0 in favor of {@code agents}
+ false
+ true
+ Path to agent jar. NOTE: a forked process is required to use this feature.
+
+
+ agents
+ java.io.File[]
+ 2.2.0
+ false
+ true
+ Path to agent jars. NOTE: a forked process is required to use this feature.
+
+
+ arguments
+ java.lang.String[]
+ 1.0.0
+ false
+ true
+ Arguments that should be passed to the application. On command line use commas to
+separate multiple arguments.
+
+
+ classesDirectory
+ java.io.File
+ 1.0.0
+ true
+ true
+ Directory containing the classes and resource files that should be packaged into
+the archive.
+
+
+ environmentVariables
+ java.util.Map
+ 2.1.0
+ false
+ true
+ List of Environment variables that should be associated with the forked process
+used to run the application. NOTE: a forked process is required to use this
+feature.
+
+
+ excludeGroupIds
+ java.lang.String
+ 1.1.0
+ false
+ true
+ Comma separated list of groupId names to exclude (exact match).
+
+
+ excludes
+ java.util.List
+ 1.1.0
+ false
+ true
+ Collection of artifact definitions to exclude. The {@link Exclude} element defines
+a {@code groupId} and {@code artifactId} mandatory properties and an optional
+{@code classifier} property.
+
+
+ folders
+ java.lang.String[]
+ 1.0.0
+ false
+ true
+ Additional folders besides the classes directory that should be added to the
+classpath.
+
+
+ fork
+ boolean
+ 1.2.0
+ false
+ true
+ Flag to indicate if the run processes should be forked. Disabling forking will
+disable some features such as an agent, custom JVM arguments, devtools or
+specifying the working directory to use.
+
+
+ includes
+ java.util.List
+ 1.2.0
+ false
+ true
+ Collection of artifact definitions to include. The {@link Include} element defines
+a {@code groupId} and {@code artifactId} mandatory properties and an optional
+{@code classifier} property.
+
+
+ jvmArguments
+ java.lang.String
+ 1.1.0
+ false
+ true
+ JVM arguments that should be associated with the forked process used to run the
+application. On command line, make sure to wrap multiple values between quotes.
+NOTE: a forked process is required to use this feature.
+
+
+ mainClass
+ java.lang.String
+ 1.0.0
+ false
+ true
+ The name of the main class. If not specified the first compiled class found that
+contains a 'main' method will be used.
+
+
+ noverify
+ boolean
+ 1.0.0
+ false
+ true
+ Flag to say that the agent requires -noverify.
+
+
+ optimizedLaunch
+ boolean
+ 2.2.0
+ false
+ true
+ Whether the JVM's launch should be optimized.
+
+
+ profiles
+ java.lang.String[]
+ 1.3.0
+ false
+ true
+ The spring profiles to activate. Convenience shortcut of specifying the
+'spring.profiles.active' argument. On command line use commas to separate multiple
+profiles.
+
+
+ project
+ org.apache.maven.project.MavenProject
+ 1.0.0
+ true
+ false
+ The Maven project.
+
+
+ skip
+ boolean
+ 1.3.2
+ false
+ true
+ Skip the execution.
+
+
+ systemPropertyVariables
+ java.util.Map
+ 2.1.0
+ false
+ true
+ List of JVM system properties to pass to the process. NOTE: a forked process is
+required to use this feature.
+
+
+ useTestClasspath
+ java.lang.Boolean
+ 1.3.0
+ false
+ true
+ Flag to include the test classpath when running.
+
+
+ workingDirectory
+ java.io.File
+ 1.5.0
+ false
+ true
+ Current working directory to use for the application. If not specified, basedir
+will be used. NOTE: a forked process is required to use this feature.
+
+
+
+ ${spring-boot.run.addResources}
+ ${spring-boot.run.agent}
+ ${spring-boot.run.agents}
+ ${spring-boot.run.arguments}
+
+ ${spring-boot.excludeGroupIds}
+ ${spring-boot.excludes}
+ ${spring-boot.run.folders}
+ ${spring-boot.run.fork}
+ ${spring-boot.includes}
+ ${spring-boot.run.jvmArguments}
+ ${spring-boot.run.main-class}
+ ${spring-boot.run.noverify}
+ ${spring-boot.run.optimizedLaunch}
+ ${spring-boot.run.profiles}
+
+ ${spring-boot.run.skip}
+ ${spring-boot.run.useTestClasspath}
+ ${spring-boot.run.workingDirectory}
+
+
+
+ start
+ Start a spring application. Contrary to the {@code run} goal, this does not block and
+allows other goal to operate on the application. This goal is typically used in
+integration test scenario where the application is started before a test suite and
+stopped after.
+ test
+ false
+ true
+ false
+ false
+ false
+ true
+ pre-integration-test
+ org.springframework.boot.maven.StartMojo
+ java
+ per-lookup
+ once-per-session
+ 1.3.0
+ false
+
+
+ addResources
+ boolean
+ 1.0.0
+ false
+ true
+ Add maven resources to the classpath directly, this allows live in-place editing of
+resources. Duplicate resources are removed from {@code target/classes} to prevent
+them to appear twice if {@code ClassLoader.getResources()} is called. Please
+consider adding {@code spring-boot-devtools} to your project instead as it provides
+this feature and many more.
+
+
+ agent
+ java.io.File[]
+ 1.0.0
+ since 2.2.0 in favor of {@code agents}
+ false
+ true
+ Path to agent jar. NOTE: a forked process is required to use this feature.
+
+
+ agents
+ java.io.File[]
+ 2.2.0
+ false
+ true
+ Path to agent jars. NOTE: a forked process is required to use this feature.
+
+
+ arguments
+ java.lang.String[]
+ 1.0.0
+ false
+ true
+ Arguments that should be passed to the application. On command line use commas to
+separate multiple arguments.
+
+
+ classesDirectory
+ java.io.File
+ 1.0.0
+ true
+ true
+ Directory containing the classes and resource files that should be packaged into
+the archive.
+
+
+ environmentVariables
+ java.util.Map
+ 2.1.0
+ false
+ true
+ List of Environment variables that should be associated with the forked process
+used to run the application. NOTE: a forked process is required to use this
+feature.
+
+
+ excludeGroupIds
+ java.lang.String
+ 1.1.0
+ false
+ true
+ Comma separated list of groupId names to exclude (exact match).
+
+
+ excludes
+ java.util.List
+ 1.1.0
+ false
+ true
+ Collection of artifact definitions to exclude. The {@link Exclude} element defines
+a {@code groupId} and {@code artifactId} mandatory properties and an optional
+{@code classifier} property.
+
+
+ folders
+ java.lang.String[]
+ 1.0.0
+ false
+ true
+ Additional folders besides the classes directory that should be added to the
+classpath.
+
+
+ fork
+ boolean
+ 1.2.0
+ false
+ true
+ Flag to indicate if the run processes should be forked. Disabling forking will
+disable some features such as an agent, custom JVM arguments, devtools or
+specifying the working directory to use.
+
+
+ includes
+ java.util.List
+ 1.2.0
+ false
+ true
+ Collection of artifact definitions to include. The {@link Include} element defines
+a {@code groupId} and {@code artifactId} mandatory properties and an optional
+{@code classifier} property.
+
+
+ jmxName
+ java.lang.String
+ false
+ true
+ The JMX name of the automatically deployed MBean managing the lifecycle of the
+spring application.
+
+
+ jmxPort
+ int
+ false
+ true
+ The port to use to expose the platform MBeanServer if the application is forked.
+
+
+ jvmArguments
+ java.lang.String
+ 1.1.0
+ false
+ true
+ JVM arguments that should be associated with the forked process used to run the
+application. On command line, make sure to wrap multiple values between quotes.
+NOTE: a forked process is required to use this feature.
+
+
+ mainClass
+ java.lang.String
+ 1.0.0
+ false
+ true
+ The name of the main class. If not specified the first compiled class found that
+contains a 'main' method will be used.
+
+
+ maxAttempts
+ int
+ false
+ true
+ The maximum number of attempts to check if the spring application is ready.
+Combined with the "wait" argument, this gives a global timeout value (30 sec by
+default)
+
+
+ noverify
+ boolean
+ 1.0.0
+ false
+ true
+ Flag to say that the agent requires -noverify.
+
+
+ profiles
+ java.lang.String[]
+ 1.3.0
+ false
+ true
+ The spring profiles to activate. Convenience shortcut of specifying the
+'spring.profiles.active' argument. On command line use commas to separate multiple
+profiles.
+
+
+ project
+ org.apache.maven.project.MavenProject
+ 1.0.0
+ true
+ false
+ The Maven project.
+
+
+ skip
+ boolean
+ 1.3.2
+ false
+ true
+ Skip the execution.
+
+
+ systemPropertyVariables
+ java.util.Map
+ 2.1.0
+ false
+ true
+ List of JVM system properties to pass to the process. NOTE: a forked process is
+required to use this feature.
+
+
+ useTestClasspath
+ java.lang.Boolean
+ 1.3.0
+ false
+ true
+ Flag to include the test classpath when running.
+
+
+ wait
+ long
+ false
+ true
+ The number of milli-seconds to wait between each attempt to check if the spring
+application is ready.
+
+
+ workingDirectory
+ java.io.File
+ 1.5.0
+ false
+ true
+ Current working directory to use for the application. If not specified, basedir
+will be used. NOTE: a forked process is required to use this feature.
+
+
+
+ ${spring-boot.run.addResources}
+ ${spring-boot.run.agent}
+ ${spring-boot.run.agents}
+ ${spring-boot.run.arguments}
+
+ ${spring-boot.excludeGroupIds}
+ ${spring-boot.excludes}
+ ${spring-boot.run.folders}
+ ${spring-boot.run.fork}
+ ${spring-boot.includes}
+ ${spring-boot.run.jvmArguments}
+ ${spring-boot.run.main-class}
+ ${spring-boot.run.noverify}
+ ${spring-boot.run.profiles}
+
+ ${spring-boot.run.skip}
+ ${spring-boot.run.useTestClasspath}
+ ${spring-boot.run.workingDirectory}
+
+
+
+ stop
+ Stop a spring application that has been started by the "start" goal. Typically invoked
+once a test suite has completed.
+ false
+ true
+ false
+ false
+ false
+ true
+ post-integration-test
+ org.springframework.boot.maven.StopMojo
+ java
+ per-lookup
+ once-per-session
+ 1.3.0
+ false
+
+
+ fork
+ java.lang.Boolean
+ 1.3.0
+ false
+ true
+ Flag to indicate if process to stop was forked. By default, the value is inherited
+from the {@link MavenProject}. If it is set, it must match the value used to
+{@link StartMojo start} the process.
+
+
+ jmxName
+ java.lang.String
+ false
+ true
+ The JMX name of the automatically deployed MBean managing the lifecycle of the
+application.
+
+
+ jmxPort
+ int
+ false
+ true
+ The port to use to lookup the platform MBeanServer if the application has been
+forked.
+
+
+ project
+ org.apache.maven.project.MavenProject
+ 1.4.1
+ true
+ false
+ The Maven project.
+
+
+ skip
+ boolean
+ 1.3.2
+ false
+ true
+ Skip the execution.
+
+
+
+ ${spring-boot.stop.fork}
+
+ ${spring-boot.stop.skip}
+
+
+
+
+
diff --git a/ci/pipeline.yml b/ci/pipeline.yml
index 73131464ca..e2aeb3871f 100644
--- a/ci/pipeline.yml
+++ b/ci/pipeline.yml
@@ -3,7 +3,7 @@ resource_types:
type: docker-image
source:
repository: springio/artifactory-resource
- tag: 0.0.10
+ tag: 0.0.11-SNAPSHOT
- name: pull-request
type: docker-image
source:
@@ -209,19 +209,9 @@ jobs:
timeout: ((task-timeout))
image: spring-boot-ci-image
file: git-repo/ci/tasks/build-project.yml
- - in_parallel:
- - task: build-smoke-tests
- timeout: ((task-timeout))
- image: spring-boot-ci-image
- file: git-repo/ci/tasks/build-smoke-tests.yml
- - task: build-integration-tests
- timeout: ((task-timeout))
- image: spring-boot-ci-image
- file: git-repo/ci/tasks/build-integration-tests.yml
- - task: build-deployment-tests
- timeout: ((task-timeout))
- image: spring-boot-ci-image
- file: git-repo/ci/tasks/build-deployment-tests.yml
+ params:
+ GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle-enterprise-cache-username))
+ GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle-enterprise-cache-password))
on_failure:
do:
- put: repo-status-build
@@ -241,16 +231,15 @@ jobs:
build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}"
build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}"
disable_checksum_uploads: true
- exclude:
- - "**/*.effective-pom"
- - "**/spring-boot-configuration-docs/**"
- - "**/spring-boot-test-support/**"
+ threads: 8
artifact_set:
- include:
- "/**/spring-boot-docs-*.zip"
properties:
"zip.type": "docs"
"zip.deployed": "false"
+ get_params:
+ threads: 8
on_failure:
do:
- put: slack-alert
@@ -283,19 +272,6 @@ jobs:
timeout: ((task-timeout))
image: spring-boot-ci-image
file: git-repo/ci/tasks/build-pr-project.yml
- - in_parallel:
- - task: build-smoke-tests
- timeout: ((task-timeout))
- image: spring-boot-ci-image
- file: git-repo/ci/tasks/build-smoke-tests.yml
- - task: build-integration-tests
- timeout: ((task-timeout))
- image: spring-boot-ci-image
- file: git-repo/ci/tasks/build-integration-tests.yml
- - task: build-deployment-tests
- timeout: ((task-timeout))
- image: spring-boot-ci-image
- file: git-repo/ci/tasks/build-deployment-tests.yml
on_success:
put: git-pull-request
params:
@@ -321,19 +297,9 @@ jobs:
timeout: ((task-timeout))
image: spring-boot-jdk11-ci-image
file: git-repo/ci/tasks/build-project.yml
- - in_parallel:
- - task: build-smoke-tests
- timeout: ((task-timeout))
- image: spring-boot-jdk11-ci-image
- file: git-repo/ci/tasks/build-smoke-tests.yml
- - task: build-integration-tests
- timeout: ((task-timeout))
- image: spring-boot-jdk11-ci-image
- file: git-repo/ci/tasks/build-integration-tests.yml
- - task: build-deployment-tests
- timeout: ((task-timeout))
- image: spring-boot-jdk11-ci-image
- file: git-repo/ci/tasks/build-deployment-tests.yml
+ params:
+ GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle-enterprise-cache-username))
+ GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle-enterprise-cache-password))
on_failure:
do:
- put: repo-status-jdk11-build
@@ -362,24 +328,14 @@ jobs:
- put: repo-status-jdk13-build
params: { state: "pending", commit: "git-repo" }
- do:
- - task: build-project
- privileged: true
- timeout: ((task-timeout))
- image: spring-boot-jdk13-ci-image
- file: git-repo/ci/tasks/build-project.yml
- - in_parallel:
- - task: build-smoke-tests
- timeout: ((task-timeout))
- image: spring-boot-jdk13-ci-image
- file: git-repo/ci/tasks/build-smoke-tests.yml
- - task: build-integration-tests
- timeout: ((task-timeout))
- image: spring-boot-jdk13-ci-image
- file: git-repo/ci/tasks/build-integration-tests.yml
- - task: build-deployment-tests
- timeout: ((task-timeout))
- image: spring-boot-jdk13-ci-image
- file: git-repo/ci/tasks/build-deployment-tests.yml
+ - task: build-project
+ privileged: true
+ timeout: ((task-timeout))
+ image: spring-boot-jdk13-ci-image
+ file: git-repo/ci/tasks/build-project.yml
+ params:
+ GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle-enterprise-cache-username))
+ GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle-enterprise-cache-password))
on_failure:
do:
- put: repo-status-jdk13-build
@@ -412,6 +368,9 @@ jobs:
tags:
- WIN64
timeout: ((task-timeout))
+ params:
+ GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle-enterprise-cache-username))
+ GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle-enterprise-cache-password))
on_failure:
do:
- put: slack-alert
@@ -437,6 +396,8 @@ jobs:
file: git-repo/ci/tasks/stage.yml
params:
RELEASE_TYPE: M
+ GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle-enterprise-cache-username))
+ GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle-enterprise-cache-password))
- put: artifactory-repo
params:
<<: *artifactory-params
@@ -455,6 +416,8 @@ jobs:
file: git-repo/ci/tasks/stage.yml
params:
RELEASE_TYPE: RC
+ GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle-enterprise-cache-username))
+ GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle-enterprise-cache-password))
- put: artifactory-repo
params:
<<: *artifactory-params
@@ -473,6 +436,8 @@ jobs:
file: git-repo/ci/tasks/stage.yml
params:
RELEASE_TYPE: RELEASE
+ GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle-enterprise-cache-username))
+ GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle-enterprise-cache-password))
- put: artifactory-repo
params:
<<: *artifactory-params
diff --git a/ci/scripts/build-deployment-tests.sh b/ci/scripts/build-deployment-tests.sh
deleted file mode 100755
index c93fd09008..0000000000
--- a/ci/scripts/build-deployment-tests.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-set -e
-
-source $(dirname $0)/common.sh
-repository=$(pwd)/distribution-repository
-
-pushd git-repo > /dev/null
-run_maven -f spring-boot-tests/spring-boot-deployment-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository}
-popd > /dev/null
diff --git a/ci/scripts/build-integration-tests.sh b/ci/scripts/build-integration-tests.sh
deleted file mode 100755
index 953eaebb1c..0000000000
--- a/ci/scripts/build-integration-tests.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-set -e
-
-source $(dirname $0)/common.sh
-repository=$(pwd)/distribution-repository
-
-pushd git-repo > /dev/null
-run_maven -f spring-boot-tests/spring-boot-integration-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository}
-popd > /dev/null
diff --git a/ci/scripts/build-project-windows.bat b/ci/scripts/build-project-windows.bat
index efade5d1ac..aa8b62e1ab 100755
--- a/ci/scripts/build-project-windows.bat
+++ b/ci/scripts/build-project-windows.bat
@@ -1,6 +1,4 @@
SET "JAVA_HOME=C:\opt\jdk-8"
SET PATH=%PATH%;C:\Program Files\Git\usr\bin
cd git-repo
-
-echo ".\mvnw clean install" > build.log
-.\mvnw clean install -U >> build.log 2>&1 || (sleep 1 && tail -n 3000 build.log && exit 1)
\ No newline at end of file
+.\gradlew --no-daemon --max-workers=4 build
diff --git a/ci/scripts/build-project.sh b/ci/scripts/build-project.sh
index da3c44e472..767884cbc9 100755
--- a/ci/scripts/build-project.sh
+++ b/ci/scripts/build-project.sh
@@ -5,6 +5,5 @@ source $(dirname $0)/common.sh
repository=$(pwd)/distribution-repository
pushd git-repo > /dev/null
-run_maven -N clean verify
-run_maven -f spring-boot-project/pom.xml clean deploy -U -Dfull -DaltDeploymentRepository=distribution::default::file://${repository}
+./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
popd > /dev/null
diff --git a/ci/scripts/build-smoke-tests.sh b/ci/scripts/build-smoke-tests.sh
deleted file mode 100755
index cfb839c0ef..0000000000
--- a/ci/scripts/build-smoke-tests.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-set -e
-
-source $(dirname $0)/common.sh
-repository=$(pwd)/distribution-repository
-
-pushd git-repo > /dev/null
-run_maven -f spring-boot-tests/spring-boot-smoke-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository}
-popd > /dev/null
diff --git a/ci/scripts/stage.sh b/ci/scripts/stage.sh
index 2ff3a71bb3..79aa2d5846 100755
--- a/ci/scripts/stage.sh
+++ b/ci/scripts/stage.sh
@@ -12,7 +12,7 @@ git clone git-repo stage-git-repo > /dev/null
pushd stage-git-repo > /dev/null
-snapshotVersion=$( get_revision_from_pom )
+snapshotVersion=$( awk -F '=' '$1 == "version" { print $2 }' gradle.properties )
if [[ $RELEASE_TYPE = "M" ]]; then
stageVersion=$( get_next_milestone_release $snapshotVersion)
nextVersion=$snapshotVersion
@@ -27,23 +27,20 @@ else
fi
echo "Staging $stageVersion (next version will be $nextVersion)"
+sed -i "s/version=$snapshotVersion/version=$stageVersion/" gradle.properties
-set_revision_to_pom "$stageVersion"
git config user.name "Spring Buildmaster" > /dev/null
git config user.email "buildmaster@springframework.org" > /dev/null
-git add pom.xml > /dev/null
+git add gradle.properties > /dev/null
git commit -m"Release v$stageVersion" > /dev/null
git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null
-run_maven -f spring-boot-project/pom.xml clean deploy -U -Dfull -DaltDeploymentRepository=distribution::default::file://${repository}
-run_maven -f spring-boot-tests/spring-boot-smoke-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository}
-run_maven -f spring-boot-tests/spring-boot-integration-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository}
-run_maven -f spring-boot-tests/spring-boot-deployment-tests/pom.xml clean install -U -Dfull -Drepository=file://${repository}
+./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
git reset --hard HEAD^ > /dev/null
if [[ $nextVersion != $snapshotVersion ]]; then
echo "Setting next development version (v$nextVersion)"
- set_revision_to_pom "$nextVersion"
+ sed -i "s/version=$snapshotVersion/version=$nextVersion/" gradle.properties
git add pom.xml > /dev/null
git commit -m"Next development version (v$nextVersion)" > /dev/null
fi;
diff --git a/ci/tasks/build-deployment-tests.yml b/ci/tasks/build-deployment-tests.yml
deleted file mode 100644
index e45a4c2a30..0000000000
--- a/ci/tasks/build-deployment-tests.yml
+++ /dev/null
@@ -1,10 +0,0 @@
----
-platform: linux
-inputs:
-- name: git-repo
-- name: distribution-repository
-caches:
-- path: maven
-- path: gradle
-run:
- path: git-repo/ci/scripts/build-deployment-tests.sh
diff --git a/ci/tasks/build-integration-tests.yml b/ci/tasks/build-integration-tests.yml
deleted file mode 100644
index bc96aa6cef..0000000000
--- a/ci/tasks/build-integration-tests.yml
+++ /dev/null
@@ -1,10 +0,0 @@
----
-platform: linux
-inputs:
-- name: git-repo
-- name: distribution-repository
-caches:
-- path: maven
-- path: gradle
-run:
- path: git-repo/ci/scripts/build-integration-tests.sh
diff --git a/ci/tasks/build-pr-project.yml b/ci/tasks/build-pr-project.yml
index b4576a4849..c3dbfc8c05 100644
--- a/ci/tasks/build-pr-project.yml
+++ b/ci/tasks/build-pr-project.yml
@@ -7,5 +7,8 @@ outputs:
caches:
- path: maven
- path: gradle
+params:
+ CI: true
+ GRADLE_ENTERPRISE_URL: https://ge.spring.io
run:
path: git-repo/ci/scripts/build-project.sh
diff --git a/ci/tasks/build-project.yml b/ci/tasks/build-project.yml
index 50e222c7d2..a22b6f5d9d 100644
--- a/ci/tasks/build-project.yml
+++ b/ci/tasks/build-project.yml
@@ -8,6 +8,11 @@ caches:
- path: maven
- path: gradle
- path: embedmongo
+params:
+ CI: true
+ GRADLE_ENTERPRISE_CACHE_USERNAME:
+ GRADLE_ENTERPRISE_CACHE_PASSWORD:
+ GRADLE_ENTERPRISE_URL: https://ge.spring.io
run:
path: bash
args:
@@ -16,4 +21,3 @@ run:
source /docker-lib.sh
start_docker
${PWD}/git-repo/ci/scripts/build-project.sh
-
diff --git a/ci/tasks/build-smoke-tests.yml b/ci/tasks/build-smoke-tests.yml
deleted file mode 100644
index a38dfb2ef9..0000000000
--- a/ci/tasks/build-smoke-tests.yml
+++ /dev/null
@@ -1,10 +0,0 @@
----
-platform: linux
-inputs:
-- name: git-repo
-- name: distribution-repository
-caches:
-- path: maven
-- path: gradle
-run:
- path: git-repo/ci/scripts/build-smoke-tests.sh
diff --git a/ci/tasks/stage.yml b/ci/tasks/stage.yml
index f486313ae9..dd21db74f8 100644
--- a/ci/tasks/stage.yml
+++ b/ci/tasks/stage.yml
@@ -7,6 +7,10 @@ outputs:
- name: distribution-repository
params:
RELEASE_TYPE:
+ CI: true
+ GRADLE_ENTERPRISE_CACHE_USERNAME:
+ GRADLE_ENTERPRISE_CACHE_PASSWORD:
+ GRADLE_ENTERPRISE_URL: https://ge.spring.io
caches:
- path: maven
- path: gradle
diff --git a/eclipse/spring-boot-project.setup b/eclipse/spring-boot-project.setup
index 79100f5284..2048d3b1bd 100644
--- a/eclipse/spring-boot-project.setup
+++ b/eclipse/spring-boot-project.setup
@@ -4,13 +4,12 @@
xmlns:xmi="http://www.omg.org/XMI"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jdt="http://www.eclipse.org/oomph/setup/jdt/1.0"
- xmlns:maven="http://www.eclipse.org/oomph/setup/maven/1.0"
xmlns:predicates="http://www.eclipse.org/oomph/predicates/1.0"
xmlns:setup="http://www.eclipse.org/oomph/setup/1.0"
xmlns:setup.p2="http://www.eclipse.org/oomph/setup/p2/1.0"
xmlns:setup.workingsets="http://www.eclipse.org/oomph/setup/workingsets/1.0"
xmlns:workingsets="http://www.eclipse.org/oomph/workingsets/1.0"
- xsi:schemaLocation="http://www.eclipse.org/oomph/setup/jdt/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/JDT.ecore http://www.eclipse.org/oomph/setup/maven/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/Maven.ecore http://www.eclipse.org/oomph/predicates/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/Predicates.ecore http://www.eclipse.org/oomph/setup/workingsets/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/SetupWorkingSets.ecore http://www.eclipse.org/oomph/workingsets/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/WorkingSets.ecore"
+ xsi:schemaLocation="http://www.eclipse.org/oomph/setup/jdt/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/JDT.ecore http://www.eclipse.org/oomph/predicates/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/Predicates.ecore http://www.eclipse.org/oomph/setup/workingsets/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/SetupWorkingSets.ecore http://www.eclipse.org/oomph/workingsets/1.0 https://git.eclipse.org/c/oomph/org.eclipse.oomph.git/plain/setups/models/WorkingSets.ecore"
name="spring.boot.2.3.x"
label="Spring Boot 2.3.x">
-
-
-
- spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin
-
-
-
search) {
+ def query = search.collect { name, value ->
+ "search.names=${encodeURL(name)}&search.values=${encodeURL(value)}"
+ }.join('&')
+
+ "$gradleEnterprise.buildScan.server/scans?$query"
+}
+
+String encodeURL(String url) {
+ URLEncoder.encode(url, 'UTF-8')
+}
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..cc4fdc293d
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..94920145f3
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000000..2fe81a7d95
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,183 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 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.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000000..9618d8d960
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,100 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/pom.xml b/pom.xml
deleted file mode 100644
index dd169ece2b..0000000000
--- a/pom.xml
+++ /dev/null
@@ -1,384 +0,0 @@
-
-
- 4.0.0
- org.springframework.boot
- spring-boot-build
- ${revision}
- pom
- Spring Boot Build
- Spring Boot Build
-
- 2.3.0.BUILD-SNAPSHOT
- ${basedir}
-
-
-
-
-
- default
-
-
- !disable-spring-boot-default-profile
-
-
-
- 0.0.17
- 0.0.4.RELEASE
-
-
-
-
- org.apache.maven.plugins
- maven-checkstyle-plugin
- 3.0.0
-
-
- com.puppycrawl.tools
- checkstyle
- 8.22
-
-
- io.spring.javaformat
- spring-javaformat-checkstyle
- ${spring-javaformat.version}
-
-
- io.spring.nohttp
- nohttp-checkstyle
- ${nohttp-checkstyle.version}
-
-
-
-
- checkstyle-validation
- validate
-
- ${disable.checks}
- src/checkstyle/checkstyle.xml
- src/checkstyle/checkstyle-suppressions.xml
- true
- main.basedir=${main.basedir}
- UTF-8
-
-
- check
-
-
-
- nohttp-checkstyle-validation
- validate
-
- ${disable.checks}
- src/checkstyle/nohttp-checkstyle.xml
- src/checkstyle/nohttp-checkstyle-suppressions.xml
- main.basedir=${main.basedir}
- UTF-8
- ${basedir}
- **/*
- **/.git/**/*,**/target/**/,**/.flattened-pom.xml,**/*.class,**/spring-boot-gradle-plugin/build/**,**/spring-boot-gradle-plugin/bin/**
-
-
- check
-
- false
-
-
-
-
- io.spring.javaformat
- spring-javaformat-maven-plugin
- ${spring-javaformat.version}
-
-
- validate
-
- ${disable.checks}
-
-
- validate
-
-
-
-
-
-
-
- spring-boot-project
-
- spring-boot-tests
-
-
-
-
- m2e
-
-
- m2e.version
-
-
-
- spring-boot-project
- spring-boot-tests
-
-
-
- repository
-
-
- repository
-
-
-
-
- repository
- ${repository}
-
- true
-
-
-
-
-
- repository
- ${repository}
-
- true
-
-
-
-
-
-
-
-
- central
- https://repo.maven.apache.org/maven2
-
- false
-
-
-
- spring-milestone
- Spring Milestone
- https://repo.spring.io/milestone
-
- false
-
-
-
- spring-snapshot
- Spring Snapshot
- https://repo.spring.io/snapshot
-
- true
-
-
-
- rabbit-milestone
- Rabbit Milestone
- https://dl.bintray.com/rabbitmq/maven-milestones
-
- false
-
-
-
-
-
- central
- https://repo.maven.apache.org/maven2
-
- false
-
-
-
- spring-release
- Spring Release
- https://repo.spring.io/release
-
-
- spring-milestone
- Spring Milestone
- https://repo.spring.io/milestone
-
- false
-
-
-
- spring-snapshot
- Spring Snapshot
- https://repo.spring.io/snapshot
-
- true
-
-
-
-
-
-
-
-
- org.eclipse.m2e
- lifecycle-mapping
- 1.0.0
-
-
-
-
-
-