Compare commits

..

No commits in common. 'main' and '2.4.x' have entirely different histories.
main ... 2.4.x

@ -1,7 +0,0 @@
# .git-blame-ignore-revs
# Reformat code following spring-javaformat upgrade
df5898a1464112f185d295d585740de696934a12
c4de86c244acdcff69ed0aecacd254399be79ce2
b07269a018a4a9d4c029aba7dd8a15fa66df681c

@ -1,6 +1,6 @@
<!-- <!--
Thanks for contributing to Spring Boot. Please review the following notes before Thanks for contributing to Spring Boot. Please review the following notes before
submitting a pull request. submitting you pull request.
Please submit only genuine pull-requests. Do not use this repository as a GitHub Please submit only genuine pull-requests. Do not use this repository as a GitHub
playground. playground.

@ -1,14 +0,0 @@
name: Print JVM thread dumps
description: Prints a thread dump for all running JVMs
runs:
using: composite
steps:
- run: |
for java_pid in $(jps -q -J-XX:+PerfDisableSharedMem); do
echo "------------------------ pid $java_pid ------------------------"
cat /proc/$java_pid/cmdline | xargs -0 echo
jcmd $java_pid Thread.print -l
jcmd $java_pid GC.heap_info
done
exit 0
shell: bash

@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

@ -1,43 +0,0 @@
name: Build Pull Request
on: pull_request
permissions:
contents: read
jobs:
build:
name: Build pull request
runs-on: ubuntu22-8-32
if: ${{ github.repository == 'spring-projects/spring-boot' }}
steps:
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'liberica'
- name: Check out code
uses: actions/checkout@v4
- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1
- name: Set up Gradle
uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a
- name: Build
env:
CI: 'true'
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --no-parallel --continue build
- name: Print JVM thread dumps when cancelled
uses: ./.github/actions/print-jvm-thread-dumps
if: cancelled()
- name: Upload build reports
uses: actions/upload-artifact@v3
if: failure()
with:
name: build-reports
path: '**/build/reports/'

@ -1,13 +1,10 @@
name: "Validate Gradle Wrapper" name: "Validate Gradle Wrapper"
on: [push, pull_request] on: [push, pull_request]
permissions:
contents: read
jobs: jobs:
validation: validation:
name: "Validation" name: "Validation"
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- uses: gradle/wrapper-validation-action@v1 - uses: gradle/wrapper-validation-action@v1

1
.gitignore vendored

@ -12,6 +12,7 @@
.classpath .classpath
.factorypath .factorypath
.gradle .gradle
.idea
.metadata .metadata
.project .project
.recommenders .recommenders

10
.idea/.gitignore vendored

@ -1,10 +0,0 @@
.name
*.xml
/modules/
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -1,121 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="AUTODETECT_INDENTS" value="false" />
<option name="OTHER_INDENT_OPTIONS">
<value>
<option name="USE_TAB_CHARACTER" value="true" />
</value>
</option>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.springframework" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
</value>
</option>
<option name="RIGHT_MARGIN" value="90" />
<option name="ENABLE_JAVADOC_FORMATTING" value="false" />
<GroovyCodeStyleSettings>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.springframework" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
</value>
</option>
</GroovyCodeStyleSettings>
<JavaCodeStyleSettings>
<option name="CLASS_NAMES_IN_JAVADOC" value="3" />
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="500" />
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
<value />
</option>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.springframework" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
</value>
</option>
<option name="ENABLE_JAVADOC_FORMATTING" value="false" />
<option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
<option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
<option name="JD_KEEP_INVALID_TAGS" value="false" />
<option name="JD_KEEP_EMPTY_LINES" value="false" />
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="java.util" alias="false" withSubpackages="false" />
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="false" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="20" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="20" />
</JetCodeStyleSettings>
<editorconfig>
<option name="ENABLED" value="false" />
</editorconfig>
<codeStyleSettings language="Groovy">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
<option name="BLANK_LINES_AROUND_FIELD" value="1" />
<option name="BLANK_LINES_AROUND_FIELD_IN_INTERFACE" value="1" />
<option name="ELSE_ON_NEW_LINE" value="true" />
<option name="CATCH_ON_NEW_LINE" value="true" />
<option name="FINALLY_ON_NEW_LINE" value="true" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
<option name="KEEP_MULTIPLE_EXPRESSIONS_IN_ONE_LINE" value="true" />
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="JSON">
<indentOptions>
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<indentOptions>
<option name="USE_TAB_CHARACTER" value="true" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

@ -1,6 +0,0 @@
<component name="CopyrightManager">
<copyright>
<option name="notice" value="Copyright 2012-&amp;#36;today.year the original author or authors.&#10;&#10;Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);&#10;you may not use this file except in compliance with the License.&#10;You may obtain a copy of the License at&#10;&#10; https://www.apache.org/licenses/LICENSE-2.0&#10;&#10;Unless required by applicable law or agreed to in writing, software&#10;distributed under the License is distributed on an &quot;AS IS&quot; BASIS,&#10;WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#10;See the License for the specific language governing permissions and&#10;limitations under the License." />
<option name="myName" value="java" />
</copyright>
</component>

@ -1,7 +0,0 @@
<component name="CopyrightManager">
<settings>
<module2copyright>
<element module="java" copyright="java" />
</module2copyright>
</settings>
</component>

@ -1,17 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="LombokGetterMayBeUsed" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NullableProblems" enabled="false" level="WARNING" enabled_by_default="false">
<option name="REPORT_NULLABLE_METHOD_OVERRIDES_NOTNULL" value="true" />
<option name="REPORT_NOT_ANNOTATED_METHOD_OVERRIDES_NOTNULL" value="true" />
<option name="REPORT_NOTNULL_PARAMETER_OVERRIDES_NULLABLE" value="true" />
<option name="REPORT_NOT_ANNOTATED_PARAMETER_OVERRIDES_NOTNULL" value="true" />
<option name="REPORT_NOT_ANNOTATED_GETTER" value="true" />
<option name="REPORT_NOT_ANNOTATED_SETTER_PARAMETER" value="true" />
<option name="REPORT_ANNOTATION_NOT_PROPAGATED_TO_OVERRIDERS" value="true" />
<option name="REPORT_NULLS_PASSED_TO_NON_ANNOTATED_METHOD" value="true" />
</inspection_tool>
<inspection_tool class="UnqualifiedFieldAccess" enabled="true" level="ERROR" enabled_by_default="true" editorAttributes="ERRORS_ATTRIBUTES" />
</profile>
</component>

@ -1,3 +0,0 @@
<component name="DependencyValidationManager">
<scope name="java" pattern="file:*.java&amp;&amp;!file:*package-info.java" />
</component>

@ -1,3 +0,0 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=17.0.8.1-librca

@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
individual is representing the project or its community. individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting a project maintainer at code-of-conduct@spring.io. All complaints will contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will
be reviewed and investigated and will result in a response that is deemed necessary and be reviewed and investigated and will result in a response that is deemed necessary and
appropriate to the circumstances. Maintainers are obligated to maintain confidentiality appropriate to the circumstances. Maintainers are obligated to maintain confidentiality
with regard to the reporter of an incident. with regard to the reporter of an incident.

@ -6,7 +6,7 @@ Spring Boot is released under the Apache 2.0 license. If you would like to contr
== Code of Conduct == Code of Conduct
This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct]. This project adheres to the Contributor Covenant link:CODE_OF_CONDUCT.adoc[code of conduct].
By participating, you are expected to uphold this code. Please report unacceptable behavior to code-of-conduct@spring.io. By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io.
@ -48,8 +48,7 @@ added after the original pull request but before a merge.
* Add some Javadocs. * Add some Javadocs.
* A few unit tests would help a lot as well -- someone has to do it. * A few unit tests would help a lot as well -- someone has to do it.
* Verification tasks, including tests and Checkstyle, can be executed by running `./gradlew check` from the project root. * Verification tasks, including tests and Checkstyle, can be executed by running `./gradlew check` from the project root.
Note that `SPRING_PROFILES_ACTIVE` environment variable might affect the result of tests, so in that case, you can prevent it by running `unset SPRING_PROFILES_ACTIVE` before running the task. * If no-one else is using your branch, please rebase it against the current master (or other target branch in the main project).
* If no-one else is using your branch, please rebase it against the current main branch (or other target branch in the project).
* When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions]. * When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions].

@ -4,7 +4,6 @@
https://www.apache.org/licenses/ https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions. 1. Definitions.

@ -1,4 +1,4 @@
= Spring Boot image:https://ci.spring.io/api/v1/teams/spring-boot/pipelines/spring-boot-3.2.x/jobs/build/badge["Build Status", link="https://ci.spring.io/teams/spring-boot/pipelines/spring-boot-3.2.x?groups=Build"] image:https://badges.gitter.im/Join Chat.svg["Chat",link="https://gitter.im/spring-projects/spring-boot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"] image:https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Gradle Enterprise", link="https://ge.spring.io/scans?&search.rootProjectNames=Spring%20Boot%20Build&search.rootProjectNames=spring-boot-build"] = Spring Boot image:https://ci.spring.io/api/v1/teams/spring-boot/pipelines/spring-boot-2.4.x/jobs/build/badge["Build Status", link="https://ci.spring.io/teams/spring-boot/pipelines/spring-boot-2.4.x?groups=Build"] image:https://badges.gitter.im/Join Chat.svg["Chat",link="https://gitter.im/spring-projects/spring-boot?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"]
:docs: https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference :docs: https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference
:github: https://github.com/spring-projects/spring-boot :github: https://github.com/spring-projects/spring-boot
@ -6,14 +6,13 @@ Spring Boot helps you to create Spring-powered, production-grade applications an
It takes an opinionated view of the Spring platform so that new and existing users can quickly get to the bits they need. It takes an opinionated view of the Spring platform so that new and existing users can quickly get to the bits they need.
You can use Spring Boot to create stand-alone Java applications that can be started using `java -jar` or more traditional WAR deployments. You can use Spring Boot to create stand-alone Java applications that can be started using `java -jar` or more traditional WAR deployments.
We also provide a command-line tool that runs Spring scripts. We also provide a command line tool that runs Spring scripts.
Our primary goals are: Our primary goals are:
* Provide a radically faster and widely accessible getting started experience for all Spring development. * Provide a radically faster and widely accessible getting started experience for all Spring development.
* Be opinionated, but get out of the way quickly as requirements start to diverge from the defaults. * Be opinionated out of the box, but get out of the way quickly as requirements start to diverge from the defaults.
* Provide a range of non-functional features common to large classes of projects (for example, embedded servers, security, metrics, health checks, externalized configuration). * Provide a range of non-functional features that are common to large classes of projects (e.g. embedded servers, security, metrics, health checks, externalized configuration).
* Absolutely no code generation and no requirement for XML configuration. * Absolutely no code generation and no requirement for XML configuration.
@ -47,14 +46,14 @@ Here is a quick teaser of a complete Spring Boot application in Java:
== Getting Help == Getting help
Are you having trouble with Spring Boot? We want to help! Having trouble with Spring Boot? We'd like to help!
* Check the {docs}/html/[reference documentation], especially the {docs}/html/howto.html#howto[How-to's] -- they provide solutions to the most common questions. * Check the {docs}/html/[reference documentation], especially the {docs}/html/howto.html#howto[How-to's] -- they provide solutions to the most common questions.
* Learn the Spring basics -- Spring Boot builds on many other Spring projects; check the https://spring.io[spring.io] website for a wealth of reference documentation. * Learn the Spring basics -- Spring Boot builds on many other Spring projects, check the https://spring.io[spring.io] web-site for a wealth of reference documentation.
If you are new to Spring, try one of the https://spring.io/guides[guides]. If you are just starting out with Spring, try one of the https://spring.io/guides[guides].
* If you are upgrading, read the {github}/wiki[release notes] for upgrade instructions and "new and noteworthy" features. * If you are upgrading, read the {github}/wiki[release notes] for upgrade instructions and "new and noteworthy" features.
* Ask a question -- we monitor https://stackoverflow.com[stackoverflow.com] for questions tagged with https://stackoverflow.com/tags/spring-boot[`spring-boot`]. * Ask a question - we monitor https://stackoverflow.com[stackoverflow.com] for questions tagged with https://stackoverflow.com/tags/spring-boot[`spring-boot`].
You can also chat with the community on https://gitter.im/spring-projects/spring-boot[Gitter]. You can also chat with the community on https://gitter.im/spring-projects/spring-boot[Gitter].
* Report bugs with Spring Boot at {github}/issues[github.com/spring-projects/spring-boot/issues]. * Report bugs with Spring Boot at {github}/issues[github.com/spring-projects/spring-boot/issues].
@ -66,17 +65,15 @@ If you want to raise an issue, please follow the recommendations below:
* Before you log a bug, please search the {github}/issues[issue tracker] to see if someone has already reported the problem. * Before you log a bug, please search the {github}/issues[issue tracker] to see if someone has already reported the problem.
* If the issue doesn't already exist, {github}/issues/new[create a new issue]. * If the issue doesn't already exist, {github}/issues/new[create a new issue].
* Please provide as much information as possible with the issue report. * Please provide as much information as possible with the issue report, we like to know the version of Spring Boot that you are using, as well as your Operating System and JVM version.
We like to know the Spring Boot version, operating system, and JVM version you're using. * If you need to paste code, or include a stack trace use Markdown +++```+++ escapes before and after your text.
* If you need to paste code or include a stack trace, use Markdown. * If possible try to create a test-case or project that replicates the problem and attach it to the issue.
+++```+++ escapes before and after your text.
* If possible, try to create a test case or project that replicates the problem and attach it to the issue.
== Building from Source == Building from Source
You don't need to build from source to use Spring Boot (binaries in https://repo.spring.io[repo.spring.io]), but if you want to try out the latest and greatest, Spring Boot can be built and published to your local Maven cache using the https://docs.gradle.org/current/userguide/gradle_wrapper.html[Gradle wrapper]. You don't need to build from source to use Spring Boot (binaries in https://repo.spring.io[repo.spring.io]), but if you want to try out the latest and greatest, Spring Boot can be built and published to your local Maven cache using the https://docs.gradle.org/current/userguide/gradle_wrapper.html[Gradle wrapper].
You also need JDK 17. You also need JDK 1.8.
[indent=0] [indent=0]
---- ----
@ -95,23 +92,23 @@ If you want to build everything, use the `build` task:
== Modules == Modules
There are several modules in Spring Boot. Here is a quick overview: There are a number of modules in Spring Boot, here is a quick overview:
=== spring-boot === spring-boot
The main library providing features that support the other parts of Spring Boot. These include: The main library providing features that support the other parts of Spring Boot, these include:
* The `SpringApplication` class, providing static convenience methods that can be used to write a stand-alone Spring Application. * The `SpringApplication` class, providing static convenience methods that can be used to write a stand-alone Spring Application.
Its sole job is to create and refresh an appropriate Spring `ApplicationContext`. Its sole job is to create and refresh an appropriate Spring `ApplicationContext`.
* Embedded web applications with a choice of container (Tomcat, Jetty, or Undertow). * Embedded web applications with a choice of container (Tomcat, Jetty or Undertow).
* First-class externalized configuration support. * First class externalized configuration support.
* Convenience `ApplicationContext` initializers, including support for sensible logging defaults. * Convenience `ApplicationContext` initializers, including support for sensible logging defaults.
=== spring-boot-autoconfigure === spring-boot-autoconfigure
Spring Boot can configure large parts of typical applications based on the content of their classpath. Spring Boot can configure large parts of common applications based on the content of their classpath.
A single `@EnableAutoConfiguration` annotation triggers auto-configuration of the Spring context. A single `@EnableAutoConfiguration` annotation triggers auto-configuration of the Spring context.
Auto-configuration attempts to deduce which beans a user might need. For example, if `HSQLDB` is on the classpath, and the user has not configured any database connections, then they probably want an in-memory database to be defined. Auto-configuration attempts to deduce which beans a user might need. For example, if `HSQLDB` is on the classpath, and the user has not configured any database connections, then they probably want an in-memory database to be defined.
@ -121,8 +118,14 @@ Auto-configuration will always back away as the user starts to define their own
=== spring-boot-starters === spring-boot-starters
Starters are a set of convenient dependency descriptors that you can include in your application. Starters are a set of convenient dependency descriptors that you can include in your application.
You get a one-stop shop for all the Spring and related technology you need without having to hunt through sample code and copy-paste loads of dependency descriptors. You get a one-stop-shop for all the Spring and related technology that you need without having to hunt through sample code and copy paste loads of dependency descriptors.
For example, if you want to get started using Spring and JPA for database access, include the `spring-boot-starter-data-jpa` dependency in your project, and you are good to go. For example, if you want to get started using Spring and JPA for database access include the `spring-boot-starter-data-jpa` dependency in your project, and you are good to go.
=== spring-boot-cli
The Spring command line application compiles and runs Groovy source, allowing you to write the absolute minimum of code to get an application running.
Spring CLI can also watch files, automatically recompiling and restarting when they change.
@ -130,7 +133,7 @@ For example, if you want to get started using Spring and JPA for database access
Actuator endpoints let you monitor and interact with your application. Actuator endpoints let you monitor and interact with your application.
Spring Boot Actuator provides the infrastructure required for actuator endpoints. Spring Boot Actuator provides the infrastructure required for actuator endpoints.
It contains annotation support for actuator endpoints. It contains annotation support for actuator endpoints.
This module provides many endpoints, including the `HealthEndpoint`, `EnvironmentEndpoint`, `BeansEndpoint`, and many more. Out of the box, this module provides a number of endpoints including the `HealthEndpoint`, `EnvironmentEndpoint`, `BeansEndpoint` and many more.
@ -148,27 +151,33 @@ This module contains core items and annotations that can be helpful when testing
=== spring-boot-test-autoconfigure === spring-boot-test-autoconfigure
Like other Spring Boot auto-configuration modules, spring-boot-test-autoconfigure provides auto-configuration for tests based on the classpath. Like other Spring Boot auto-configuration modules, spring-boot-test-autoconfigure, provides auto-configuration for tests based on the classpath.
It includes many annotations that can automatically configure a slice of your application that needs to be tested. It includes a number of annotations that can be used to automatically configure a slice of your application that needs to be tested.
=== spring-boot-loader === spring-boot-loader
Spring Boot Loader provides the secret sauce that allows you to build a single jar file that can be launched using `java -jar`. Spring Boot Loader provides the secret sauce that allows you to build a single jar file that can be launched using `java -jar`.
Generally, you will not need to use `spring-boot-loader` directly but work with the link:spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin[Gradle] or link:spring-boot-project/spring-boot-tools/spring-boot-maven-plugin[Maven] plugin instead. Generally you will not need to use `spring-boot-loader` directly, but instead work with the link:spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin[Gradle] or link:spring-boot-project/spring-boot-tools/spring-boot-maven-plugin[Maven] plugin.
=== spring-boot-devtools === spring-boot-devtools
The spring-boot-devtools module provides additional development-time features, such as automatic restarts, for a smoother application development experience. The spring-boot-devtools module provides additional development-time features such as automatic restarts, for a smoother application development experience.
Developer tools are automatically disabled when running a fully packaged application. Developer tools are automatically disabled when running a fully packaged application.
== Samples
Groovy samples for use with the command line application are available in link:spring-boot-project/spring-boot-cli/samples[spring-boot-cli/samples].
To run the CLI samples type `spring run <sample>.groovy` from samples directory.
== Guides == Guides
The https://spring.io/[spring.io] site contains several guides that show how to use Spring Boot step-by-step: The https://spring.io/[spring.io] site contains several guides that show how to use Spring Boot step-by-step:
* https://spring.io/guides/gs/spring-boot/[Building an Application with Spring Boot] is an introductory guide that shows you how to create an application, run it, and add some management services. * https://spring.io/guides/gs/spring-boot/[Building an Application with Spring Boot] is a very basic guide that shows you how to create an application, run it and add some management services.
* https://spring.io/guides/gs/actuator-service/[Building a RESTful Web Service with Spring Boot Actuator] is a guide to creating a REST web service and also shows how the server can be configured. * https://spring.io/guides/gs/actuator-service/[Building a RESTful Web Service with Spring Boot Actuator] is a guide to creating a REST web service and also shows how the server can be configured.
* https://spring.io/guides/gs/convert-jar-to-war/[Converting a Spring Boot JAR Application to a WAR] shows you how to run applications in a web server as a WAR file. * https://spring.io/guides/gs/convert-jar-to-war/[Converting a Spring Boot JAR Application to a WAR] shows you how to run applications in a web server as a WAR file.

@ -1,13 +1,10 @@
plugins { plugins {
id "base"
id "org.jetbrains.kotlin.jvm" apply false // https://youtrack.jetbrains.com/issue/KT-30276 id "org.jetbrains.kotlin.jvm" apply false // https://youtrack.jetbrains.com/issue/KT-30276
id "io.spring.nohttp" version "0.0.11" id "io.spring.nohttp" version "0.0.10"
} }
description = "Spring Boot Build" description = "Spring Boot Build"
defaultTasks 'build' defaultTasks 'build'
nohttp { nohttp {
@ -16,13 +13,9 @@ nohttp {
source.exclude "**/build/**" source.exclude "**/build/**"
source.exclude "**/out/**" source.exclude "**/out/**"
source.exclude "**/target/**" source.exclude "**/target/**"
source.exclude "**/.settings/**"
source.exclude "**/.classpath"
source.exclude "**/.project"
source.exclude "spring-boot-project/spring-boot-tools/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/docker/export.tar"
} }
check { task build {
dependsOn checkstyleNohttp dependsOn checkstyleNohttp
} }
@ -43,7 +36,3 @@ allprojects {
resolutionStrategy.cacheChangingModulesFor 0, "minutes" resolutionStrategy.cacheChangingModulesFor 0, "minutes"
} }
} }
tasks.named("checkstyleNohttp").configure {
maxHeapSize = "1536m"
}

@ -2,64 +2,40 @@ plugins {
id "java-gradle-plugin" id "java-gradle-plugin"
id "io.spring.javaformat" version "${javaFormatVersion}" id "io.spring.javaformat" version "${javaFormatVersion}"
id "checkstyle" id "checkstyle"
id "eclipse"
} }
repositories { repositories {
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
maven { url "https://repo.spring.io/release" }
} }
sourceCompatibility = 17 sourceCompatibility = 1.8
targetCompatibility = 17 targetCompatibility = 1.8
def versions = [:]
new File(projectDir.parentFile, "gradle.properties").withInputStream {
def properties = new Properties()
properties.load(it)
["assertj", "commonsCodec", "hamcrest", "jackson", "junitJupiter",
"kotlin", "maven"].each {
versions[it] = properties[it + "Version"]
}
}
versions["springFramework"] = "6.0.12"
ext.set("versions", versions)
if (versions.springFramework.contains("-")) {
repositories {
maven { url "https://repo.spring.io/milestone" }
maven { url "https://repo.spring.io/snapshot" }
}
}
dependencies { dependencies {
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}" checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
implementation("com.fasterxml.jackson.core:jackson-databind:2.11.4")
implementation(platform("org.springframework:spring-framework-bom:${versions.springFramework}")) implementation("commons-codec:commons-codec:1.13")
implementation("com.diffplug.gradle:goomph:3.37.2") implementation("org.apache.maven:maven-embedder:3.6.2")
implementation("com.fasterxml.jackson.core:jackson-databind:${versions.jackson}") implementation("org.asciidoctor:asciidoctor-gradle-jvm:3.0.0")
implementation("com.gradle:gradle-enterprise-gradle-plugin:3.12.1") implementation("org.asciidoctor:asciidoctor-gradle-jvm-pdf:3.0.0")
implementation("com.tngtech.archunit:archunit:1.0.0") implementation("org.gradle:test-retry-gradle-plugin:1.1.9")
implementation("commons-codec:commons-codec:${versions.commonsCodec}") implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.31")
implementation("de.undercouch.download:de.undercouch.download.gradle.plugin:5.5.0") implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.4.31")
implementation("org.springframework:spring-core:5.2.2.RELEASE")
implementation("org.springframework:spring-web:5.2.2.RELEASE")
implementation("io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}") implementation("io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}")
implementation("org.apache.maven:maven-embedder:${versions.maven}") testImplementation("org.assertj:assertj-core:3.11.1")
implementation("org.asciidoctor:asciidoctor-gradle-jvm:3.3.2") testImplementation("org.apache.logging.log4j:log4j-core:2.12.1")
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}") testImplementation("org.junit.jupiter:junit-jupiter:5.6.0")
implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${versions.kotlin}") testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.6.3")
implementation("org.springframework:spring-context")
implementation("org.springframework:spring-core")
implementation("org.springframework:spring-web")
testImplementation("org.assertj:assertj-core:${versions.assertj}")
testImplementation("org.hamcrest:hamcrest:${versions.hamcrest}")
testImplementation("org.junit.jupiter:junit-jupiter:${versions.junitJupiter}")
testImplementation("org.springframework:spring-test")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
} }
checkstyle { checkstyle {
toolVersion = "10.12.4" 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 { gradlePlugin {
@ -68,10 +44,6 @@ gradlePlugin {
id = "org.springframework.boot.annotation-processor" id = "org.springframework.boot.annotation-processor"
implementationClass = "org.springframework.boot.build.processors.AnnotationProcessorPlugin" implementationClass = "org.springframework.boot.build.processors.AnnotationProcessorPlugin"
} }
architecturePlugin {
id = "org.springframework.boot.architecture"
implementationClass = "org.springframework.boot.build.architecture.ArchitecturePlugin"
}
autoConfigurationPlugin { autoConfigurationPlugin {
id = "org.springframework.boot.auto-configuration" id = "org.springframework.boot.auto-configuration"
implementationClass = "org.springframework.boot.build.autoconfigure.AutoConfigurationPlugin" implementationClass = "org.springframework.boot.build.autoconfigure.AutoConfigurationPlugin"
@ -96,10 +68,6 @@ gradlePlugin {
id = "org.springframework.boot.integration-test" id = "org.springframework.boot.integration-test"
implementationClass = "org.springframework.boot.build.test.IntegrationTestPlugin" implementationClass = "org.springframework.boot.build.test.IntegrationTestPlugin"
} }
systemTestPlugin {
id = "org.springframework.boot.system-test"
implementationClass = "org.springframework.boot.build.test.SystemTestPlugin"
}
mavenPluginPlugin { mavenPluginPlugin {
id = "org.springframework.boot.maven-plugin" id = "org.springframework.boot.maven-plugin"
implementationClass = "org.springframework.boot.build.mavenplugin.MavenPluginPlugin" implementationClass = "org.springframework.boot.build.mavenplugin.MavenPluginPlugin"
@ -112,10 +80,6 @@ gradlePlugin {
id = "org.springframework.boot.optional-dependencies" id = "org.springframework.boot.optional-dependencies"
implementationClass = "org.springframework.boot.build.optional.OptionalDependenciesPlugin" implementationClass = "org.springframework.boot.build.optional.OptionalDependenciesPlugin"
} }
processedAnnotationsPlugin {
id = "org.springframework.boot.processed-annotations"
implementationClass = "org.springframework.boot.build.processors.ProcessedAnnotationsPlugin"
}
starterPlugin { starterPlugin {
id = "org.springframework.boot.starter" id = "org.springframework.boot.starter"
implementationClass = "org.springframework.boot.build.starters.StarterPlugin" implementationClass = "org.springframework.boot.build.starters.StarterPlugin"
@ -130,10 +94,3 @@ gradlePlugin {
test { test {
useJUnitPlatform() useJUnitPlatform()
} }
eclipse.classpath.file.whenMerged {
def jreEntry = entries.find { it.path.contains("org.eclipse.jdt.launching.JRE_CONTAINER") }
jreEntry.entryAttributes['module'] = 'true'
jreEntry.entryAttributes['limit-modules'] = 'java.base'
}

@ -1,9 +0,0 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="com.puppycrawl.tools.checkstyle.Checker">
<module name="io.spring.javaformat.checkstyle.SpringChecks">
<property name="excludes" value="com.puppycrawl.tools.checkstyle.checks.javadoc.JavadocPackageCheck" />
</module>
</module>

@ -1 +1 @@
javaFormatVersion=0.0.38 javaFormatVersion=0.0.29

@ -3,4 +3,11 @@ pluginManagement {
mavenCentral() mavenCentral()
gradlePluginPortal() gradlePluginPortal()
} }
resolutionStrategy {
eachPlugin {
if (requested.id.id == "io.spring.javaformat") {
useModule "io.spring.javaformat:spring-javaformat-gradle-plugin:${requested.version}"
}
}
}
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +17,7 @@
package org.springframework.boot.build; package org.springframework.boot.build;
import java.io.File; import java.io.File;
import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
@ -25,12 +26,19 @@ import org.asciidoctor.gradle.jvm.AbstractAsciidoctorTask;
import org.asciidoctor.gradle.jvm.AsciidoctorJExtension; import org.asciidoctor.gradle.jvm.AsciidoctorJExtension;
import org.asciidoctor.gradle.jvm.AsciidoctorJPlugin; import org.asciidoctor.gradle.jvm.AsciidoctorJPlugin;
import org.asciidoctor.gradle.jvm.AsciidoctorTask; import org.asciidoctor.gradle.jvm.AsciidoctorTask;
import org.gradle.api.JavaVersion; import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.Project; 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.PathSensitivity; import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.Sync; import org.gradle.api.tasks.Sync;
import org.gradle.api.tasks.TaskAction;
import org.springframework.boot.build.artifacts.ArtifactRelease; import org.springframework.boot.build.artifactory.ArtifactoryRepository;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -38,14 +46,23 @@ import org.springframework.util.StringUtils;
* the plugin is applied: * the plugin is applied:
* *
* <ul> * <ul>
* <li>The {@code https://repo.spring.io/release} Maven repository is configured and
* limited to dependencies in the following groups:
* <ul>
* <li>{@code io.spring.asciidoctor}
* <li>{@code io.spring.docresources}
* </ul>
* <li>All warnings are made fatal. * <li>All warnings are made fatal.
* <li>The version of AsciidoctorJ is upgraded to 2.4.3. * <li>The version of AsciidoctorJ is upgraded to 2.4.1.
* <li>An {@code asciidoctorExtensions} configuration is created. * <li>An {@code asciidoctorExtensions} configuration is created.
* <li>A task is created to resolve and unzip our documentation resources (CSS and
* Javascript).
* <li>For each {@link AsciidoctorTask} (HTML only): * <li>For each {@link AsciidoctorTask} (HTML only):
* <ul> * <ul>
* <li>A task is created to sync the documentation resources to its output directory. * <li>A task is created to sync the documentation resources to its output directory.
* <li>{@code doctype} {@link AsciidoctorTask#options(Map) option} is configured. * <li>{@code doctype} {@link AsciidoctorTask#options(Map) option} is configured.
* <li>The {@code backend} is configured. * <li>{@link AsciidoctorTask#attributes(Map) Attributes} are configured for syntax
* highlighting, CSS styling, docinfo, etc.
* </ul> * </ul>
* <li>For each {@link AbstractAsciidoctorTask} (HTML and PDF): * <li>For each {@link AbstractAsciidoctorTask} (HTML and PDF):
* <ul> * <ul>
@ -59,22 +76,53 @@ import org.springframework.util.StringUtils;
* </ul> * </ul>
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Scott Frederick
*/ */
class AsciidoctorConventions { class AsciidoctorConventions {
private static final String ASCIIDOCTORJ_VERSION = "2.4.3"; private static final String ASCIIDOCTORJ_VERSION = "2.4.1";
private static final String EXTENSIONS_CONFIGURATION_NAME = "asciidoctorExtensions"; private static final String EXTENSIONS_CONFIGURATION_NAME = "asciidoctorExtensions";
void apply(Project project) { void apply(Project project) {
project.getPlugins().withType(AsciidoctorJPlugin.class, (asciidoctorPlugin) -> { project.getPlugins().withType(AsciidoctorJPlugin.class, (asciidoctorPlugin) -> {
configureDocumentationDependenciesRepository(project);
makeAllWarningsFatal(project); makeAllWarningsFatal(project);
upgradeAsciidoctorJVersion(project); upgradeAsciidoctorJVersion(project);
createAsciidoctorExtensionsConfiguration(project); Configuration asciidoctorExtensions = createAsciidoctorExtensionsConfiguration(project);
project.getTasks() UnzipDocumentationResources unzipResources = createUnzipDocumentationResourcesTask(project);
.withType(AbstractAsciidoctorTask.class, project.getTasks().withType(AbstractAsciidoctorTask.class, (asciidoctorTask) -> {
(asciidoctorTask) -> configureAsciidoctorTask(project, asciidoctorTask)); configureCommonAttributes(project, asciidoctorTask);
configureOptions(asciidoctorTask);
asciidoctorTask.baseDirFollowsSourceDir();
asciidoctorTask.configurations(asciidoctorExtensions);
Sync syncSource = createSyncDocumentationSourceTask(project, asciidoctorTask);
if (asciidoctorTask instanceof AsciidoctorTask) {
configureHtmlOnlyAttributes(asciidoctorTask);
syncSource.from(unzipResources, (resources) -> resources.into("asciidoc"));
asciidoctorTask.doFirst(new Action<Task>() {
@Override
public void execute(Task task) {
project.copy((spec) -> {
spec.from(asciidoctorTask.getSourceDir());
spec.into(asciidoctorTask.getOutputDir());
spec.include("css/**", "js/**");
});
}
});
}
});
});
}
private void configureDocumentationDependenciesRepository(Project project) {
project.getRepositories().maven((mavenRepo) -> {
mavenRepo.setUrl(URI.create("https://repo.spring.io/release"));
mavenRepo.mavenContent((mavenContent) -> {
mavenContent.includeGroup("io.spring.asciidoctor");
mavenContent.includeGroup("io.spring.docresources");
});
}); });
} }
@ -86,74 +134,103 @@ class AsciidoctorConventions {
project.getExtensions().getByType(AsciidoctorJExtension.class).setVersion(ASCIIDOCTORJ_VERSION); project.getExtensions().getByType(AsciidoctorJExtension.class).setVersion(ASCIIDOCTORJ_VERSION);
} }
private void createAsciidoctorExtensionsConfiguration(Project project) { private Configuration createAsciidoctorExtensionsConfiguration(Project project) {
project.getConfigurations().create(EXTENSIONS_CONFIGURATION_NAME, (configuration) -> { return project.getConfigurations().create(EXTENSIONS_CONFIGURATION_NAME,
project.getConfigurations() (configuration) -> project.getConfigurations()
.matching((candidate) -> "dependencyManagement".equals(candidate.getName())) .matching((candidate) -> "dependencyManagement".equals(candidate.getName()))
.all(configuration::extendsFrom); .all((dependencyManagement) -> configuration.extendsFrom(dependencyManagement)));
configuration.getDependencies()
.add(project.getDependencies()
.create("io.spring.asciidoctor.backends:spring-asciidoctor-backends:0.0.5"));
configuration.getDependencies()
.add(project.getDependencies().create("org.asciidoctor:asciidoctorj-pdf:1.5.3"));
});
} }
private void configureAsciidoctorTask(Project project, AbstractAsciidoctorTask asciidoctorTask) { private UnzipDocumentationResources createUnzipDocumentationResourcesTask(Project project) {
asciidoctorTask.configurations(EXTENSIONS_CONFIGURATION_NAME); Configuration documentationResources = project.getConfigurations().maybeCreate("documentationResources");
configureCommonAttributes(project, asciidoctorTask); documentationResources.getDependencies()
configureOptions(asciidoctorTask); .add(project.getDependencies().create("io.spring.docresources:spring-doc-resources:0.2.5"));
configureForkOptions(asciidoctorTask); UnzipDocumentationResources unzipResources = project.getTasks().create("unzipDocumentationResources",
asciidoctorTask.baseDirFollowsSourceDir(); UnzipDocumentationResources.class);
createSyncDocumentationSourceTask(project, asciidoctorTask); unzipResources.setResources(documentationResources);
if (asciidoctorTask instanceof AsciidoctorTask task) { unzipResources.setOutputDir(new File(project.getBuildDir(), "docs/resources"));
boolean pdf = task.getName().toLowerCase().contains("pdf"); return unzipResources;
String backend = (!pdf) ? "spring-html" : "spring-pdf"; }
task.outputOptions((outputOptions) -> outputOptions.backends(backend));
} 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.getInputs().dir(syncedSource).withPathSensitivity(PathSensitivity.RELATIVE)
.withPropertyName("synced source");
asciidoctorTask.setSourceDir(project.relativePath(new File(syncedSource, "asciidoc/")));
return syncDocumentationSource;
}
private void configureOptions(AbstractAsciidoctorTask asciidoctorTask) {
asciidoctorTask.options(Collections.singletonMap("doctype", "book"));
}
private void configureHtmlOnlyAttributes(AbstractAsciidoctorTask asciidoctorTask) {
Map<String, Object> attributes = new HashMap<>();
attributes.put("source-highlighter", "highlightjs");
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) { private void configureCommonAttributes(Project project, AbstractAsciidoctorTask asciidoctorTask) {
ArtifactRelease artifacts = ArtifactRelease.forProject(project);
Map<String, Object> attributes = new HashMap<>(); Map<String, Object> attributes = new HashMap<>();
attributes.put("attribute-missing", "warn"); attributes.put("attribute-missing", "warn");
attributes.put("github-tag", determineGitHubTag(project)); attributes.put("github-tag", determineGitHubTag(project));
attributes.put("artifact-release-type", artifacts.getType()); attributes.put("spring-boot-artifactory-repo", ArtifactoryRepository.forProject(project));
attributes.put("artifact-download-repo", artifacts.getDownloadRepo());
attributes.put("revnumber", null); attributes.put("revnumber", null);
asciidoctorTask.attributes(attributes); asciidoctorTask.attributes(attributes);
} }
// See https://github.com/asciidoctor/asciidoctor-gradle-plugin/issues/597
private void configureForkOptions(AbstractAsciidoctorTask asciidoctorTask) {
if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_16)) {
asciidoctorTask.forkOptions((options) -> options.jvmArgs("--add-opens", "java.base/sun.nio.ch=ALL-UNNAMED",
"--add-opens", "java.base/java.io=ALL-UNNAMED"));
}
}
private String determineGitHubTag(Project project) { private String determineGitHubTag(Project project) {
String version = "v" + project.getVersion(); String version = "v" + project.getVersion();
return (version.endsWith("-SNAPSHOT")) ? "main" : version; return (version.endsWith("-SNAPSHOT")) ? "2.4.x" : version;
} }
private void configureOptions(AbstractAsciidoctorTask asciidoctorTask) { /**
asciidoctorTask.options(Collections.singletonMap("doctype", "book")); * {@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));
}
});
}
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.getInputs()
.dir(syncedSource)
.withPathSensitivity(PathSensitivity.RELATIVE)
.withPropertyName("synced source");
asciidoctorTask.setSourceDir(project.relativePath(new File(syncedSource, "asciidoc/")));
return syncDocumentationSource;
} }
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -47,8 +47,6 @@ public class ConventionsPlugin implements Plugin<Project> {
new MavenPublishingConventions().apply(project); new MavenPublishingConventions().apply(project);
new AsciidoctorConventions().apply(project); new AsciidoctorConventions().apply(project);
new KotlinConventions().apply(project); new KotlinConventions().apply(project);
new WarConventions().apply(project);
new EclipseConventions().apply(project);
} }
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -38,24 +38,23 @@ public class DeployedPlugin implements Plugin<Project> {
public static final String GENERATE_POM_TASK_NAME = "generatePomFileForMavenPublication"; public static final String GENERATE_POM_TASK_NAME = "generatePomFileForMavenPublication";
@Override @Override
@SuppressWarnings("deprecation")
public void apply(Project project) { public void apply(Project project) {
project.getPlugins().apply(MavenPublishPlugin.class); project.getPlugins().apply(MavenPublishPlugin.class);
project.getPlugins().apply(MavenRepositoryPlugin.class); project.getPlugins().apply(MavenRepositoryPlugin.class);
PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class); PublishingExtension publishing = project.getExtensions().getByType(PublishingExtension.class);
MavenPublication mavenPublication = publishing.getPublications().create("maven", MavenPublication.class); MavenPublication mavenPublication = publishing.getPublications().create("maven", MavenPublication.class);
project.afterEvaluate((evaluated) -> project.getPlugins().withType(JavaPlugin.class).all((javaPlugin) -> { project.afterEvaluate((evaluated) -> {
if (((Jar) project.getTasks().getByName(JavaPlugin.JAR_TASK_NAME)).isEnabled()) { project.getPlugins().withType(JavaPlugin.class).all((javaPlugin) -> {
project.getComponents() if (((Jar) project.getTasks().getByName(JavaPlugin.JAR_TASK_NAME)).isEnabled()) {
.matching((component) -> component.getName().equals("java")) project.getComponents().matching((component) -> component.getName().equals("java"))
.all(mavenPublication::from); .all((javaComponent) -> mavenPublication.from(javaComponent));
} }
})); });
project.getPlugins() });
.withType(JavaPlatformPlugin.class) project.getPlugins().withType(JavaPlatformPlugin.class)
.all((javaPlugin) -> project.getComponents() .all((javaPlugin) -> project.getComponents()
.matching((component) -> component.getName().equals("javaPlatform")) .matching((component) -> component.getName().equals("javaPlatform"))
.all(mavenPublication::from)); .all((javaComponent) -> mavenPublication.from(javaComponent)));
} }
} }

@ -1,69 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build;
import org.gradle.api.Project;
import org.gradle.plugins.ide.api.XmlFileContentMerger;
import org.gradle.plugins.ide.eclipse.EclipsePlugin;
import org.gradle.plugins.ide.eclipse.model.Classpath;
import org.gradle.plugins.ide.eclipse.model.ClasspathEntry;
import org.gradle.plugins.ide.eclipse.model.EclipseClasspath;
import org.gradle.plugins.ide.eclipse.model.EclipseModel;
import org.gradle.plugins.ide.eclipse.model.Library;
/**
* Conventions that are applied in the presence of the {@link EclipsePlugin} to work
* around buildship issue {@code #1238}.
*
* @author Phillip Webb
*/
class EclipseConventions {
void apply(Project project) {
project.getPlugins().withType(EclipsePlugin.class, (eclipse) -> {
EclipseModel eclipseModel = project.getExtensions().getByType(EclipseModel.class);
eclipseModel.classpath(this::configureClasspath);
});
}
private void configureClasspath(EclipseClasspath classpath) {
classpath.file(this::configureClasspathFile);
}
private void configureClasspathFile(XmlFileContentMerger merger) {
merger.whenMerged((content) -> {
if (content instanceof Classpath classpath) {
classpath.getEntries().removeIf(this::isKotlinPluginContributedBuildDirectory);
}
});
}
private boolean isKotlinPluginContributedBuildDirectory(ClasspathEntry entry) {
return (entry instanceof Library library) && isKotlinPluginContributedBuildDirectory(library.getPath())
&& isTest(library);
}
private boolean isKotlinPluginContributedBuildDirectory(String path) {
return path.contains("/main") && (path.contains("/build/classes/") || path.contains("/build/resources/"));
}
private boolean isTest(Library library) {
Object value = library.getEntryAttributes().get("test");
return (value instanceof String string && Boolean.parseBoolean(string));
}
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2022 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -87,7 +87,8 @@ public class ExtractResources extends DefaultTask {
throw new GradleException("Resource '" + resourceName + "' does not exist"); throw new GradleException("Resource '" + resourceName + "' does not exist");
} }
String resource = FileCopyUtils.copyToString(new InputStreamReader(resourceStream, StandardCharsets.UTF_8)); String resource = FileCopyUtils.copyToString(new InputStreamReader(resourceStream, StandardCharsets.UTF_8));
resource = this.propertyPlaceholderHelper.replacePlaceholders(resource, this.properties::get); resource = this.propertyPlaceholderHelper.replacePlaceholders(resource,
(placeholder) -> this.properties.get(placeholder));
FileCopyUtils.copy(resource, FileCopyUtils.copy(resource,
new FileWriter(this.destinationDirectory.file(resourceName).get().getAsFile())); new FileWriter(this.destinationDirectory.file(resourceName).get().getAsFile()));
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,8 +24,6 @@ import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import com.gradle.enterprise.gradleplugin.testretry.TestRetryExtension;
import com.gradle.enterprise.gradleplugin.testselection.PredictiveTestSelectionExtension;
import io.spring.javaformat.gradle.SpringJavaFormatPlugin; import io.spring.javaformat.gradle.SpringJavaFormatPlugin;
import io.spring.javaformat.gradle.tasks.CheckFormat; import io.spring.javaformat.gradle.tasks.CheckFormat;
import io.spring.javaformat.gradle.tasks.Format; import io.spring.javaformat.gradle.tasks.Format;
@ -47,9 +45,9 @@ import org.gradle.api.tasks.bundling.Jar;
import org.gradle.api.tasks.compile.JavaCompile; import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.api.tasks.javadoc.Javadoc; import org.gradle.api.tasks.javadoc.Javadoc;
import org.gradle.api.tasks.testing.Test; import org.gradle.api.tasks.testing.Test;
import org.gradle.external.javadoc.CoreJavadocOptions; import org.gradle.testretry.TestRetryPlugin;
import org.gradle.testretry.TestRetryTaskExtension;
import org.springframework.boot.build.architecture.ArchitecturePlugin;
import org.springframework.boot.build.classpath.CheckClasspathForProhibitedDependencies; import org.springframework.boot.build.classpath.CheckClasspathForProhibitedDependencies;
import org.springframework.boot.build.optional.OptionalDependenciesPlugin; import org.springframework.boot.build.optional.OptionalDependenciesPlugin;
import org.springframework.boot.build.testing.TestFailuresPlugin; import org.springframework.boot.build.testing.TestFailuresPlugin;
@ -61,27 +59,24 @@ import org.springframework.util.StringUtils;
* plugin is applied: * plugin is applied:
* *
* <ul> * <ul>
* <li>The project is configured with source and target compatibility of 17 * <li>The project is configered with source and target compatibility of 1.8
* <li>{@link SpringJavaFormatPlugin Spring Java Format}, {@link CheckstylePlugin * <li>{@link SpringJavaFormatPlugin Spring Java Format}, {@link CheckstylePlugin
* Checkstyle}, {@link TestFailuresPlugin Test Failures}, and {@link ArchitecturePlugin * Checkstyle}, {@link TestFailuresPlugin Test Failures}, and {@link TestRetryPlugin Test
* Architecture} plugins are applied * Retry} plugins are applied
* <li>{@link Test} tasks are configured: * <li>{@link Test} tasks are configured:
* <ul> * <ul>
* <li>to use JUnit Platform * <li>to use JUnit Platform
* <li>with a max heap of 1024M * <li>with a max heap of 1024M
* <li>to run after any Checkstyle and format checking tasks * <li>to run after any Checkstyle and format checking tasks
* <li>to enable retries with a maximum of three attempts when running on CI
* <li>to use predictive test selection when the value of the
* {@code ENABLE_PREDICTIVE_TEST_SELECTION} environment variable is {@code true}
* </ul> * </ul>
* <li>A {@code testRuntimeOnly} dependency upon * <li>A {@code testRuntimeOnly} dependency upon
* {@code org.junit.platform:junit-platform-launcher} is added to projects with the * {@code org.junit.platform:junit-platform-launcher} is added to projects with the
* {@link JavaPlugin} applied * {@link JavaPlugin} applied
* <li>{@link JavaCompile}, {@link Javadoc}, and {@link Format} tasks are configured to * <li>{@link JavaCompile}, {@link Javadoc}, and {@link Format} tasks are configured to
* use UTF-8 encoding * use UTF-8 encoding
* <li>{@link JavaCompile} tasks are configured to: * <li>{@link JavaCompile} tasks are configured to use {@code -parameters}.
* <li>When building with Java 8, {@link JavaCompile} tasks are also configured to:
* <ul> * <ul>
* <li>Use {@code -parameters}.
* <li>Treat warnings as errors * <li>Treat warnings as errors
* <li>Enable {@code unchecked}, {@code deprecation}, {@code rawtypes}, and {@code varags} * <li>Enable {@code unchecked}, {@code deprecation}, {@code rawtypes}, and {@code varags}
* warnings * warnings
@ -107,12 +102,11 @@ import org.springframework.util.StringUtils;
*/ */
class JavaConventions { class JavaConventions {
private static final String SOURCE_AND_TARGET_COMPATIBILITY = "17"; private static final String SOURCE_AND_TARGET_COMPATIBILITY = "1.8";
void apply(Project project) { void apply(Project project) {
project.getPlugins().withType(JavaBasePlugin.class, (java) -> { project.getPlugins().withType(JavaBasePlugin.class, (java) -> {
project.getPlugins().apply(TestFailuresPlugin.class); project.getPlugins().apply(TestFailuresPlugin.class);
project.getPlugins().apply(ArchitecturePlugin.class);
configureSpringJavaFormat(project); configureSpringJavaFormat(project);
configureJavaConventions(project); configureJavaConventions(project);
configureJavadocConventions(project); configureJavadocConventions(project);
@ -125,18 +119,16 @@ class JavaConventions {
} }
private void configureJarManifestConventions(Project project) { private void configureJarManifestConventions(Project project) {
ExtractResources extractLegalResources = project.getTasks() ExtractResources extractLegalResources = project.getTasks().create("extractLegalResources",
.create("extractLegalResources", ExtractResources.class); ExtractResources.class);
extractLegalResources.getDestinationDirectory().set(project.getLayout().getBuildDirectory().dir("legal")); extractLegalResources.getDestinationDirectory().set(project.getLayout().getBuildDirectory().dir("legal"));
extractLegalResources.setResourcesNames(Arrays.asList("LICENSE.txt", "NOTICE.txt")); extractLegalResources.setResourcesNames(Arrays.asList("LICENSE.txt", "NOTICE.txt"));
extractLegalResources.property("version", project.getVersion().toString()); extractLegalResources.property("version", project.getVersion().toString());
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
Set<String> sourceJarTaskNames = sourceSets.stream() Set<String> sourceJarTaskNames = sourceSets.stream().map(SourceSet::getSourcesJarTaskName)
.map(SourceSet::getSourcesJarTaskName) .collect(Collectors.toSet());
.collect(Collectors.toSet()); Set<String> javadocJarTaskNames = sourceSets.stream().map(SourceSet::getJavadocJarTaskName)
Set<String> javadocJarTaskNames = sourceSets.stream() .collect(Collectors.toSet());
.map(SourceSet::getJavadocJarTaskName)
.collect(Collectors.toSet());
project.getTasks().withType(Jar.class, (jar) -> project.afterEvaluate((evaluated) -> { project.getTasks().withType(Jar.class, (jar) -> project.afterEvaluate((evaluated) -> {
jar.metaInf((metaInf) -> metaInf.from(extractLegalResources)); jar.metaInf((metaInf) -> metaInf.from(extractLegalResources));
jar.manifest((manifest) -> { jar.manifest((manifest) -> {
@ -167,45 +159,30 @@ class JavaConventions {
project.getTasks().withType(Test.class, (test) -> { project.getTasks().withType(Test.class, (test) -> {
test.useJUnitPlatform(); test.useJUnitPlatform();
test.setMaxHeapSize("1024M"); test.setMaxHeapSize("1024M");
project.getTasks().withType(Checkstyle.class, test::mustRunAfter); project.getTasks().withType(Checkstyle.class, (checkstyle) -> test.mustRunAfter(checkstyle));
project.getTasks().withType(CheckFormat.class, test::mustRunAfter); project.getTasks().withType(CheckFormat.class, (checkFormat) -> test.mustRunAfter(checkFormat));
configureTestRetries(test);
configurePredictiveTestSelection(test);
}); });
project.getPlugins() project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> project.getDependencies()
.withType(JavaPlugin.class, (javaPlugin) -> project.getDependencies()
.add(JavaPlugin.TEST_RUNTIME_ONLY_CONFIGURATION_NAME, "org.junit.platform:junit-platform-launcher")); .add(JavaPlugin.TEST_RUNTIME_ONLY_CONFIGURATION_NAME, "org.junit.platform:junit-platform-launcher"));
project.getPlugins().apply(TestRetryPlugin.class);
project.getTasks().withType(Test.class,
(test) -> project.getPlugins().withType(TestRetryPlugin.class, (testRetryPlugin) -> {
TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class);
testRetry.getFailOnPassedAfterRetry().set(true);
testRetry.getMaxRetries().set(isCi() ? 3 : 0);
}));
} }
private void configureTestRetries(Test test) { private boolean buildingWithJava8(Project project) {
TestRetryExtension testRetry = test.getExtensions().getByType(TestRetryExtension.class); return (!project.hasProperty("buildJavaHome")) && JavaVersion.current() == JavaVersion.VERSION_1_8;
testRetry.getFailOnPassedAfterRetry().set(false);
testRetry.getMaxRetries().set(isCi() ? 3 : 0);
} }
private boolean isCi() { private boolean isCi() {
return Boolean.parseBoolean(System.getenv("CI")); return Boolean.parseBoolean(System.getenv("CI"));
} }
private void configurePredictiveTestSelection(Test test) {
if (isPredictiveTestSelectionEnabled()) {
PredictiveTestSelectionExtension predictiveTestSelection = test.getExtensions()
.getByType(PredictiveTestSelectionExtension.class);
predictiveTestSelection.getEnabled().convention(true);
}
}
private boolean isPredictiveTestSelectionEnabled() {
return Boolean.parseBoolean(System.getenv("ENABLE_PREDICTIVE_TEST_SELECTION"));
}
private void configureJavadocConventions(Project project) { private void configureJavadocConventions(Project project) {
project.getTasks().withType(Javadoc.class, (javadoc) -> { project.getTasks().withType(Javadoc.class, (javadoc) -> javadoc.getOptions().source("1.8").encoding("UTF-8"));
CoreJavadocOptions options = (CoreJavadocOptions) javadoc.getOptions();
options.source("17");
options.encoding("UTF-8");
options.addStringOption("Xdoclint:none", "-quiet");
});
} }
private void configureJavaConventions(Project project) { private void configureJavaConventions(Project project) {
@ -223,17 +200,13 @@ class JavaConventions {
compile.setSourceCompatibility(SOURCE_AND_TARGET_COMPATIBILITY); compile.setSourceCompatibility(SOURCE_AND_TARGET_COMPATIBILITY);
compile.setTargetCompatibility(SOURCE_AND_TARGET_COMPATIBILITY); compile.setTargetCompatibility(SOURCE_AND_TARGET_COMPATIBILITY);
} }
else if (buildingWithJava17(project)) { else if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
args.addAll(Arrays.asList("-Werror", "-Xlint:unchecked", "-Xlint:deprecation", "-Xlint:rawtypes", args.addAll(Arrays.asList("-Werror", "-Xlint:unchecked", "-Xlint:deprecation", "-Xlint:rawtypes",
"-Xlint:varargs")); "-Xlint:varargs"));
} }
}); });
} }
private boolean buildingWithJava17(Project project) {
return !project.hasProperty("toolchainVersion") && JavaVersion.current() == JavaVersion.VERSION_17;
}
private void configureSpringJavaFormat(Project project) { private void configureSpringJavaFormat(Project project) {
project.getPlugins().apply(SpringJavaFormatPlugin.class); project.getPlugins().apply(SpringJavaFormatPlugin.class);
project.getTasks().withType(Format.class, (Format) -> Format.setEncoding("UTF-8")); project.getTasks().withType(Format.class, (Format) -> Format.setEncoding("UTF-8"));
@ -244,7 +217,7 @@ class JavaConventions {
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
checkstyleDependencies checkstyleDependencies
.add(project.getDependencies().create("io.spring.javaformat:spring-javaformat-checkstyle:" + version)); .add(project.getDependencies().create("io.spring.javaformat:spring-javaformat-checkstyle:" + version));
} }
private void configureDependencyManagement(Project project) { private void configureDependencyManagement(Project project) {
@ -255,18 +228,14 @@ class JavaConventions {
configuration.setCanBeResolved(false); configuration.setCanBeResolved(false);
}); });
configurations configurations
.matching((configuration) -> configuration.getName().endsWith("Classpath") .matching((configuration) -> configuration.getName().endsWith("Classpath")
|| JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.equals(configuration.getName())) || JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME.equals(configuration.getName()))
.all((configuration) -> configuration.extendsFrom(dependencyManagement)); .all((configuration) -> configuration.extendsFrom(dependencyManagement));
Dependency springBootParent = project.getDependencies() Dependency springBootParent = project.getDependencies().enforcedPlatform(project.getDependencies()
.enforcedPlatform(project.getDependencies()
.project(Collections.singletonMap("path", ":spring-boot-project:spring-boot-parent"))); .project(Collections.singletonMap("path", ":spring-boot-project:spring-boot-parent")));
dependencyManagement.getDependencies().add(springBootParent); dependencyManagement.getDependencies().add(springBootParent);
project.getPlugins() project.getPlugins().withType(OptionalDependenciesPlugin.class, (optionalDependencies) -> configurations
.withType(OptionalDependenciesPlugin.class, .getByName(OptionalDependenciesPlugin.OPTIONAL_CONFIGURATION_NAME).extendsFrom(dependencyManagement));
(optionalDependencies) -> configurations
.getByName(OptionalDependenciesPlugin.OPTIONAL_CONFIGURATION_NAME)
.extendsFrom(dependencyManagement));
} }
private void configureToolchain(Project project) { private void configureToolchain(Project project) {
@ -288,9 +257,9 @@ class JavaConventions {
} }
private void createProhibitedDependenciesCheck(Configuration classpath, Project project) { private void createProhibitedDependenciesCheck(Configuration classpath, Project project) {
CheckClasspathForProhibitedDependencies checkClasspathForProhibitedDependencies = project.getTasks() CheckClasspathForProhibitedDependencies checkClasspathForProhibitedDependencies = project.getTasks().create(
.create("check" + StringUtils.capitalize(classpath.getName() + "ForProhibitedDependencies"), "check" + StringUtils.capitalize(classpath.getName() + "ForProhibitedDependencies"),
CheckClasspathForProhibitedDependencies.class); CheckClasspathForProhibitedDependencies.class);
checkClasspathForProhibitedDependencies.setClasspath(classpath); checkClasspathForProhibitedDependencies.setClasspath(classpath);
project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForProhibitedDependencies); project.getTasks().getByName(JavaBasePlugin.CHECK_TASK_NAME).dependsOn(checkClasspathForProhibitedDependencies);
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,9 +16,6 @@
package org.springframework.boot.build; package org.springframework.boot.build;
import java.util.ArrayList;
import java.util.List;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions; import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions;
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile; import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
@ -30,10 +27,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
* <ul> * <ul>
* <li>{@link KotlinCompile} tasks are configured to: * <li>{@link KotlinCompile} tasks are configured to:
* <ul> * <ul>
* <li>Use {@code apiVersion} and {@code languageVersion} 1.7. * <li>Use {@code apiVersion} and {@code languageVersion} 1.3.
* <li>Use {@code jvmTarget} 17.
* <li>Treat all warnings as errors * <li>Treat all warnings as errors
* <li>Suppress version warnings
* </ul> * </ul>
* </ul> * </ul>
* *
@ -44,20 +39,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
class KotlinConventions { class KotlinConventions {
void apply(Project project) { void apply(Project project) {
project.getPlugins() project.getPlugins().withId("org.jetbrains.kotlin.jvm", (plugin) -> {
.withId("org.jetbrains.kotlin.jvm", project.getTasks().withType(KotlinCompile.class, (compile) -> {
(plugin) -> project.getTasks().withType(KotlinCompile.class, this::configure)); KotlinJvmOptions kotlinOptions = compile.getKotlinOptions();
} kotlinOptions.setApiVersion("1.3");
kotlinOptions.setLanguageVersion("1.3");
private void configure(KotlinCompile compile) { kotlinOptions.setAllWarningsAsErrors(true);
KotlinJvmOptions kotlinOptions = compile.getKotlinOptions(); });
kotlinOptions.setApiVersion("1.7"); });
kotlinOptions.setLanguageVersion("1.7");
kotlinOptions.setJvmTarget("17");
kotlinOptions.setAllWarningsAsErrors(true);
List<String> freeCompilerArgs = new ArrayList<>(compile.getKotlinOptions().getFreeCompilerArgs());
freeCompilerArgs.add("-Xsuppress-version-warnings");
compile.getKotlinOptions().setFreeCompilerArgs(freeCompilerArgs);
} }
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,9 +22,9 @@ import org.gradle.api.attributes.Usage;
import org.gradle.api.component.AdhocComponentWithVariants; import org.gradle.api.component.AdhocComponentWithVariants;
import org.gradle.api.component.ConfigurationVariantDetails; import org.gradle.api.component.ConfigurationVariantDetails;
import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.publish.PublishingExtension; import org.gradle.api.publish.PublishingExtension;
import org.gradle.api.publish.VariantVersionMappingStrategy;
import org.gradle.api.publish.maven.MavenPom; import org.gradle.api.publish.maven.MavenPom;
import org.gradle.api.publish.maven.MavenPomDeveloperSpec; import org.gradle.api.publish.maven.MavenPomDeveloperSpec;
import org.gradle.api.publish.maven.MavenPomIssueManagement; import org.gradle.api.publish.maven.MavenPomIssueManagement;
@ -67,9 +67,8 @@ class MavenPublishingConventions {
mavenRepository.setName("deployment"); mavenRepository.setName("deployment");
}); });
} }
publishing.getPublications() publishing.getPublications().withType(MavenPublication.class)
.withType(MavenPublication.class) .all((mavenPublication) -> customizeMavenPublication(mavenPublication, project));
.all((mavenPublication) -> customizeMavenPublication(mavenPublication, project));
project.getPlugins().withType(JavaPlugin.class).all((javaPlugin) -> { project.getPlugins().withType(JavaPlugin.class).all((javaPlugin) -> {
JavaPluginExtension extension = project.getExtensions().getByType(JavaPluginExtension.class); JavaPluginExtension extension = project.getExtensions().getByType(JavaPluginExtension.class);
extension.withJavadocJar(); extension.withJavadocJar();
@ -80,9 +79,8 @@ class MavenPublishingConventions {
private void customizeMavenPublication(MavenPublication publication, Project project) { private void customizeMavenPublication(MavenPublication publication, Project project) {
customizePom(publication.getPom(), project); customizePom(publication.getPom(), project);
project.getPlugins() project.getPlugins().withType(JavaPlugin.class)
.withType(JavaPlugin.class) .all((javaPlugin) -> customizeJavaMavenPublication(publication, project));
.all((javaPlugin) -> customizeJavaMavenPublication(publication, project));
suppressMavenOptionalFeatureWarnings(publication); suppressMavenOptionalFeatureWarnings(publication);
} }
@ -104,9 +102,9 @@ class MavenPublishingConventions {
private void customizeJavaMavenPublication(MavenPublication publication, Project project) { private void customizeJavaMavenPublication(MavenPublication publication, Project project) {
addMavenOptionalFeature(project); addMavenOptionalFeature(project);
publication.versionMapping((strategy) -> strategy.usage(Usage.JAVA_API, (mappingStrategy) -> mappingStrategy publication.versionMapping((strategy) -> strategy.usage(Usage.JAVA_API, (mappingStrategy) -> mappingStrategy
.fromResolutionOf(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME))); .fromResolutionOf(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)));
publication.versionMapping( publication.versionMapping((strategy) -> strategy.usage(Usage.JAVA_RUNTIME,
(strategy) -> strategy.usage(Usage.JAVA_RUNTIME, VariantVersionMappingStrategy::fromResolutionResult)); (mappingStrategy) -> mappingStrategy.fromResolutionResult()));
} }
/** /**
@ -116,10 +114,11 @@ class MavenPublishingConventions {
*/ */
private void addMavenOptionalFeature(Project project) { private void addMavenOptionalFeature(Project project) {
JavaPluginExtension extension = project.getExtensions().getByType(JavaPluginExtension.class); JavaPluginExtension extension = project.getExtensions().getByType(JavaPluginExtension.class);
JavaPluginConvention convention = project.getConvention().getPlugin(JavaPluginConvention.class);
extension.registerFeature("mavenOptional", extension.registerFeature("mavenOptional",
(feature) -> feature.usingSourceSet(extension.getSourceSets().getByName("main"))); (feature) -> feature.usingSourceSet(convention.getSourceSets().getByName("main")));
AdhocComponentWithVariants javaComponent = (AdhocComponentWithVariants) project.getComponents() AdhocComponentWithVariants javaComponent = (AdhocComponentWithVariants) project.getComponents()
.findByName("java"); .findByName("java");
javaComponent.addVariantsFromConfiguration( javaComponent.addVariantsFromConfiguration(
project.getConfigurations().findByName("mavenOptionalRuntimeElements"), project.getConfigurations().findByName("mavenOptionalRuntimeElements"),
ConfigurationVariantDetails::mapToOptional); ConfigurationVariantDetails::mapToOptional);
@ -131,7 +130,7 @@ class MavenPublishingConventions {
} }
private void customizeOrganization(MavenPomOrganization organization) { private void customizeOrganization(MavenPomOrganization organization) {
organization.getName().set("VMware, Inc."); organization.getName().set("Pivotal Software, Inc.");
organization.getUrl().set("https://spring.io"); organization.getUrl().set("https://spring.io");
} }
@ -144,9 +143,9 @@ class MavenPublishingConventions {
private void customizeDevelopers(MavenPomDeveloperSpec developers) { private void customizeDevelopers(MavenPomDeveloperSpec developers) {
developers.developer((developer) -> { developers.developer((developer) -> {
developer.getName().set("Spring"); developer.getName().set("Pivotal");
developer.getEmail().set("ask@spring.io"); developer.getEmail().set("info@pivotal.io");
developer.getOrganization().set("VMware, Inc."); developer.getOrganization().set("Pivotal Software, Inc.");
developer.getOrganizationUrl().set("https://www.spring.io"); developer.getOrganizationUrl().set("https://www.spring.io");
}); });
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -61,45 +61,36 @@ public class MavenRepositoryPlugin implements Plugin<Project> {
mavenRepository.setName("project"); mavenRepository.setName("project");
mavenRepository.setUrl(repositoryLocation.toURI()); mavenRepository.setUrl(repositoryLocation.toURI());
}); });
project.getTasks() project.getTasks().matching((task) -> task.getName().equals(PUBLISH_TO_PROJECT_REPOSITORY_TASK_NAME))
.matching((task) -> task.getName().equals(PUBLISH_TO_PROJECT_REPOSITORY_TASK_NAME)) .all((task) -> setUpProjectRepository(project, task, repositoryLocation));
.all((task) -> setUpProjectRepository(project, task, repositoryLocation)); project.getTasks().matching((task) -> task.getName().equals("publishPluginMavenPublicationToProjectRepository"))
project.getTasks() .all((task) -> setUpProjectRepository(project, task, repositoryLocation));
.matching((task) -> task.getName().equals("publishPluginMavenPublicationToProjectRepository"))
.all((task) -> setUpProjectRepository(project, task, repositoryLocation));
} }
private void setUpProjectRepository(Project project, Task publishTask, File repositoryLocation) { private void setUpProjectRepository(Project project, Task publishTask, File repositoryLocation) {
publishTask.doFirst(new CleanAction(repositoryLocation)); publishTask.doFirst(new CleanAction(repositoryLocation));
Configuration projectRepository = project.getConfigurations().create(MAVEN_REPOSITORY_CONFIGURATION_NAME); Configuration projectRepository = project.getConfigurations().create(MAVEN_REPOSITORY_CONFIGURATION_NAME);
project.getArtifacts() project.getArtifacts().add(projectRepository.getName(), repositoryLocation,
.add(projectRepository.getName(), repositoryLocation, (artifact) -> artifact.builtBy(publishTask)); (artifact) -> artifact.builtBy(publishTask));
DependencySet target = projectRepository.getDependencies(); DependencySet target = projectRepository.getDependencies();
project.getPlugins() project.getPlugins().withType(JavaPlugin.class).all((javaPlugin) -> addMavenRepositoryDependencies(project,
.withType(JavaPlugin.class) JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME, target));
.all((javaPlugin) -> addMavenRepositoryDependencies(project, JavaPlugin.IMPLEMENTATION_CONFIGURATION_NAME, project.getPlugins().withType(JavaLibraryPlugin.class)
target)); .all((javaLibraryPlugin) -> addMavenRepositoryDependencies(project, JavaPlugin.API_CONFIGURATION_NAME,
project.getPlugins() target));
.withType(JavaLibraryPlugin.class) project.getPlugins().withType(JavaPlatformPlugin.class)
.all((javaLibraryPlugin) -> addMavenRepositoryDependencies(project, JavaPlugin.API_CONFIGURATION_NAME, .all((javaPlugin) -> addMavenRepositoryDependencies(project, JavaPlatformPlugin.API_CONFIGURATION_NAME,
target)); target));
project.getPlugins()
.withType(JavaPlatformPlugin.class)
.all((javaPlugin) -> addMavenRepositoryDependencies(project, JavaPlatformPlugin.API_CONFIGURATION_NAME,
target));
} }
private void addMavenRepositoryDependencies(Project project, String sourceConfigurationName, DependencySet target) { private void addMavenRepositoryDependencies(Project project, String sourceConfigurationName, DependencySet target) {
project.getConfigurations() project.getConfigurations().getByName(sourceConfigurationName).getDependencies()
.getByName(sourceConfigurationName) .withType(ProjectDependency.class).all((dependency) -> {
.getDependencies() Map<String, String> dependencyDescriptor = new HashMap<>();
.withType(ProjectDependency.class) dependencyDescriptor.put("path", dependency.getDependencyProject().getPath());
.all((dependency) -> { dependencyDescriptor.put("configuration", MAVEN_REPOSITORY_CONFIGURATION_NAME);
Map<String, String> dependencyDescriptor = new HashMap<>(); target.add(project.getDependencies().project(dependencyDescriptor));
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<Task> { private static final class CleanAction implements Action<Task> {

@ -1,5 +1,5 @@
/* /*
* Copyright 2021-2023 the original author or authors. * Copyright 2021-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -44,7 +44,7 @@ public class SyncAppSource extends DefaultTask {
this.sourceDirectory = objects.directoryProperty(); this.sourceDirectory = objects.directoryProperty();
this.destinationDirectory = objects.directoryProperty(); this.destinationDirectory = objects.directoryProperty();
this.pluginVersion = objects.property(String.class) this.pluginVersion = objects.property(String.class)
.convention(getProject().provider(() -> getProject().getVersion().toString())); .convention(getProject().provider(() -> getProject().getVersion().toString()));
} }
@TaskAction @TaskAction

@ -1,60 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build;
import java.util.ArrayList;
import java.util.List;
import org.gradle.api.JavaVersion;
import org.gradle.api.Project;
import org.gradle.api.internal.IConventionAware;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.plugins.ide.eclipse.EclipseWtpPlugin;
import org.gradle.plugins.ide.eclipse.model.EclipseModel;
import org.gradle.plugins.ide.eclipse.model.Facet;
/**
* Conventions that are applied in the presence of the {WarPlugin}. When the plugin is
* applied:
* <ul>
* <li>Update Eclipse WTP Plugin facets to use Servlet 5.0</li>
* </ul>
*
* @author Phillip Webb
*/
public class WarConventions {
void apply(Project project) {
project.getPlugins().withType(EclipseWtpPlugin.class, (wtp) -> {
project.getTasks().getByName(EclipseWtpPlugin.ECLIPSE_WTP_FACET_TASK_NAME).doFirst((task) -> {
EclipseModel eclipseModel = project.getExtensions().getByType(EclipseModel.class);
((IConventionAware) eclipseModel.getWtp().getFacet()).getConventionMapping()
.map("facets", () -> getFacets(project));
});
});
}
private List<Facet> getFacets(Project project) {
JavaVersion javaVersion = project.getExtensions().getByType(JavaPluginExtension.class).getSourceCompatibility();
List<Facet> facets = new ArrayList<>();
facets.add(new Facet(Facet.FacetType.fixed, "jst.web", null));
facets.add(new Facet(Facet.FacetType.installed, "jst.web", "5.0"));
facets.add(new Facet(Facet.FacetType.installed, "jst.java", javaVersion.toString()));
return facets;
}
}

@ -1,226 +0,0 @@
/*
* Copyright 2022-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.architecture;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.stream.Collectors;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClass.Predicates;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaParameter;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.EvaluationResult;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Task;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.tasks.IgnoreEmptyDirectories;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
/**
* {@link Task} that checks for architecture problems.
*
* @author Andy Wilkinson
*/
public abstract class ArchitectureCheck extends DefaultTask {
private FileCollection classes;
public ArchitectureCheck() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
getRules().addAll(allPackagesShouldBeFreeOfTangles(),
allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization(),
allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters(),
noClassesShouldCallStepVerifierStepVerifyComplete(),
noClassesShouldConfigureDefaultStepVerifierTimeout(), noClassesShouldCallCollectorsToList());
getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList()));
}
@TaskAction
void checkArchitecture() throws IOException {
JavaClasses javaClasses = new ClassFileImporter()
.importPaths(this.classes.getFiles().stream().map(File::toPath).toList());
List<EvaluationResult> violations = getRules().get()
.stream()
.map((rule) -> rule.evaluate(javaClasses))
.filter(EvaluationResult::hasViolation)
.toList();
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
outputFile.getParentFile().mkdirs();
if (!violations.isEmpty()) {
StringBuilder report = new StringBuilder();
for (EvaluationResult violation : violations) {
report.append(violation.getFailureReport().toString());
report.append(String.format("%n"));
}
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
throw new GradleException("Architecture check failed. See '" + outputFile + "' for details.");
}
else {
outputFile.createNewFile();
}
}
private ArchRule allPackagesShouldBeFreeOfTangles() {
return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles();
}
private ArchRule allBeanPostProcessorBeanMethodsShouldBeStaticAndHaveParametersThatWillNotCausePrematureInitialization() {
return ArchRuleDefinition.methods()
.that()
.areAnnotatedWith("org.springframework.context.annotation.Bean")
.and()
.haveRawReturnType(Predicates.assignableTo("org.springframework.beans.factory.config.BeanPostProcessor"))
.should(onlyHaveParametersThatWillNotCauseEagerInitialization())
.andShould()
.beStatic()
.allowEmptyShould(true);
}
private ArchCondition<JavaMethod> onlyHaveParametersThatWillNotCauseEagerInitialization() {
DescribedPredicate<CanBeAnnotated> notAnnotatedWithLazy = DescribedPredicate
.not(CanBeAnnotated.Predicates.annotatedWith("org.springframework.context.annotation.Lazy"));
DescribedPredicate<JavaClass> notOfASafeType = DescribedPredicate
.not(Predicates.assignableTo("org.springframework.beans.factory.ObjectProvider")
.or(Predicates.assignableTo("org.springframework.context.ApplicationContext"))
.or(Predicates.assignableTo("org.springframework.core.env.Environment")));
return new ArchCondition<>("not have parameters that will cause eager initialization") {
@Override
public void check(JavaMethod item, ConditionEvents events) {
item.getParameters()
.stream()
.filter(notAnnotatedWithLazy)
.filter((parameter) -> notOfASafeType.test(parameter.getRawType()))
.forEach((parameter) -> events.add(SimpleConditionEvent.violated(parameter,
parameter.getDescription() + " will cause eager initialization as it is "
+ notAnnotatedWithLazy.getDescription() + " and is "
+ notOfASafeType.getDescription())));
}
};
}
private ArchRule allBeanFactoryPostProcessorBeanMethodsShouldBeStaticAndHaveNoParameters() {
return ArchRuleDefinition.methods()
.that()
.areAnnotatedWith("org.springframework.context.annotation.Bean")
.and()
.haveRawReturnType(
Predicates.assignableTo("org.springframework.beans.factory.config.BeanFactoryPostProcessor"))
.should(haveNoParameters())
.andShould()
.beStatic()
.allowEmptyShould(true);
}
private ArchCondition<JavaMethod> haveNoParameters() {
return new ArchCondition<>("have no parameters") {
@Override
public void check(JavaMethod item, ConditionEvents events) {
List<JavaParameter> parameters = item.getParameters();
if (!parameters.isEmpty()) {
events
.add(SimpleConditionEvent.violated(item, item.getDescription() + " should have no parameters"));
}
}
};
}
private ArchRule noClassesShouldCallStepVerifierStepVerifyComplete() {
return ArchRuleDefinition.noClasses()
.should()
.callMethod("reactor.test.StepVerifier$Step", "verifyComplete")
.because("it can block indefinitely and expectComplete().verify(Duration) should be used instead");
}
private ArchRule noClassesShouldConfigureDefaultStepVerifierTimeout() {
return ArchRuleDefinition.noClasses()
.should()
.callMethod("reactor.test.StepVerifier", "setDefaultTimeout", "java.time.Duration")
.because("expectComplete().verify(Duration) should be used instead");
}
private ArchRule noClassesShouldCallCollectorsToList() {
return ArchRuleDefinition.noClasses()
.should()
.callMethod(Collectors.class, "toList")
.because("java.util.stream.Stream.toList() should be used instead");
}
public void setClasses(FileCollection classes) {
this.classes = classes;
}
@Internal
public FileCollection getClasses() {
return this.classes;
}
@InputFiles
@SkipWhenEmpty
@IgnoreEmptyDirectories
@PathSensitive(PathSensitivity.RELATIVE)
final FileTree getInputClasses() {
return this.classes.getAsFileTree();
}
@Optional
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
public abstract DirectoryProperty getResourcesDirectory();
@OutputDirectory
public abstract DirectoryProperty getOutputDirectory();
@Internal
public abstract ListProperty<ArchRule> getRules();
@Input
// The rules themselves can't be an input as they aren't serializable so we use their
// descriptions instead
abstract ListProperty<String> getRuleDescriptions();
}

@ -1,66 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.architecture;
import java.util.ArrayList;
import java.util.List;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.springframework.util.StringUtils;
/**
* {@link Plugin} for verifying a project's architecture.
*
* @author Andy Wilkinson
*/
public class ArchitecturePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().withType(JavaBasePlugin.class, (javaPlugin) -> registerTasks(project));
}
private void registerTasks(Project project) {
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
List<TaskProvider<ArchitectureCheck>> packageTangleChecks = new ArrayList<>();
for (SourceSet sourceSet : javaPluginExtension.getSourceSets()) {
TaskProvider<ArchitectureCheck> checkPackageTangles = project.getTasks()
.register("checkArchitecture" + StringUtils.capitalize(sourceSet.getName()), ArchitectureCheck.class,
(task) -> {
task.setClasses(sourceSet.getOutput().getClassesDirs());
task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir());
task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName()
+ " source set.");
task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
});
packageTangleChecks.add(checkPackageTangles);
}
if (!packageTangleChecks.isEmpty()) {
TaskProvider<Task> checkTask = project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME);
checkTask.configure((check) -> check.dependsOn(packageTangleChecks));
}
}
}

@ -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.artifactory;
import org.gradle.api.Project;
/**
* An Artifactory repository to which a build of Spring Boot can be published.
*
* @author Andy Wilkinson
*/
public final class ArtifactoryRepository {
private final String name;
private ArtifactoryRepository(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
@Override
public String toString() {
return this.name;
}
public static ArtifactoryRepository forProject(Project project) {
return new ArtifactoryRepository(determineArtifactoryRepo(project));
}
private static String determineArtifactoryRepo(Project project) {
String version = project.getVersion().toString();
int modifierIndex = version.lastIndexOf('-');
if (modifierIndex == -1) {
return "release";
}
String type = version.substring(modifierIndex + 1);
if (type.startsWith("M") || type.startsWith("RC")) {
return "milestone";
}
return "snapshot";
}
}

@ -1,74 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.artifacts;
import org.gradle.api.Project;
/**
* Information about artifacts produced by a build.
*
* @author Andy Wilkinson
* @author Scott Frederick
*/
public final class ArtifactRelease {
private static final String SNAPSHOT = "snapshot";
private static final String MILESTONE = "milestone";
private static final String RELEASE = "release";
private static final String SPRING_REPO = "https://repo.spring.io/%s";
private static final String MAVEN_REPO = "https://repo.maven.apache.org/maven2";
private final String type;
private ArtifactRelease(String type) {
this.type = type;
}
public String getType() {
return this.type;
}
public String getDownloadRepo() {
return (this.isRelease()) ? MAVEN_REPO : String.format(SPRING_REPO, this.getType());
}
public boolean isRelease() {
return RELEASE.equals(this.type);
}
public static ArtifactRelease forProject(Project project) {
return new ArtifactRelease(determineReleaseType(project));
}
private static String determineReleaseType(Project project) {
String version = project.getVersion().toString();
int modifierIndex = version.lastIndexOf('-');
if (modifierIndex == -1) {
return RELEASE;
}
String type = version.substring(modifierIndex + 1);
if (type.startsWith("M") || type.startsWith("RC")) {
return MILESTONE;
}
return SNAPSHOT;
}
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,16 +16,14 @@
package org.springframework.boot.build.autoconfigure; package org.springframework.boot.build.autoconfigure;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileReader; import java.io.FileReader;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.util.Collections; import java.io.Reader;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
@ -40,32 +38,28 @@ import org.gradle.api.tasks.TaskAction;
import org.springframework.asm.ClassReader; import org.springframework.asm.ClassReader;
import org.springframework.asm.Opcodes; import org.springframework.asm.Opcodes;
import org.springframework.core.CollectionFactory; import org.springframework.core.CollectionFactory;
import org.springframework.util.StringUtils;
/** /**
* A {@link Task} for generating metadata describing a project's auto-configuration * A {@link Task} for generating metadata describing a project's auto-configuration
* classes. * classes.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Scott Frederick
*/ */
public class AutoConfigurationMetadata extends DefaultTask { public class AutoConfigurationMetadata extends DefaultTask {
private static final String COMMENT_START = "#";
private SourceSet sourceSet; private SourceSet sourceSet;
private File outputFile; private File outputFile;
public AutoConfigurationMetadata() { public AutoConfigurationMetadata() {
getInputs() getInputs()
.file((Callable<File>) () -> new File(this.sourceSet.getOutput().getResourcesDir(), .file((Callable<File>) () -> new File(this.sourceSet.getOutput().getResourcesDir(),
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports")) "META-INF/spring.factories"))
.withPathSensitivity(PathSensitivity.RELATIVE) .withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("spring.factories");
.withPropertyName("org.springframework.boot.autoconfigure.AutoConfiguration");
dependsOn((Callable<String>) () -> this.sourceSet.getProcessResourcesTaskName()); dependsOn((Callable<String>) () -> this.sourceSet.getProcessResourcesTaskName());
getProject().getConfigurations() getProject().getConfigurations()
.maybeCreate(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME); .maybeCreate(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME);
} }
public void setSourceSet(SourceSet sourceSet) { public void setSourceSet(SourceSet sourceSet) {
@ -92,7 +86,11 @@ public class AutoConfigurationMetadata extends DefaultTask {
private Properties readAutoConfiguration() throws IOException { private Properties readAutoConfiguration() throws IOException {
Properties autoConfiguration = CollectionFactory.createSortedProperties(true); Properties autoConfiguration = CollectionFactory.createSortedProperties(true);
List<String> classNames = readAutoConfigurationsFile(); Properties springFactories = readSpringFactories(
new File(this.sourceSet.getOutput().getResourcesDir(), "META-INF/spring.factories"));
String enableAutoConfiguration = springFactories
.getProperty("org.springframework.boot.autoconfigure.EnableAutoConfiguration");
Set<String> classNames = StringUtils.commaDelimitedListToSet(enableAutoConfiguration);
Set<String> publicClassNames = new LinkedHashSet<>(); Set<String> publicClassNames = new LinkedHashSet<>();
for (String className : classNames) { for (String className : classNames) {
File classFile = findClassFile(className); File classFile = findClassFile(className);
@ -111,30 +109,6 @@ public class AutoConfigurationMetadata extends DefaultTask {
return autoConfiguration; return autoConfiguration;
} }
/**
* Reads auto-configurations from
* META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.
* @return auto-configurations
*/
private List<String> readAutoConfigurationsFile() throws IOException {
File file = new File(this.sourceSet.getOutput().getResourcesDir(),
"META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports");
if (!file.exists()) {
return Collections.emptyList();
}
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
return reader.lines().map(this::stripComment).filter((line) -> !line.isEmpty()).toList();
}
}
private String stripComment(String line) {
int commentStart = line.indexOf(COMMENT_START);
if (commentStart == -1) {
return line.trim();
}
return line.substring(0, commentStart).trim();
}
private File findClassFile(String className) { private File findClassFile(String className) {
String classFileName = className.replace(".", "/") + ".class"; String classFileName = className.replace(".", "/") + ".class";
for (File classesDir : this.sourceSet.getOutput().getClassesDirs()) { for (File classesDir : this.sourceSet.getOutput().getClassesDirs()) {
@ -146,4 +120,12 @@ public class AutoConfigurationMetadata extends DefaultTask {
return null; return null;
} }
private Properties readSpringFactories(File file) throws IOException {
Properties springFactories = new Properties();
try (Reader in = new FileReader(file)) {
springFactories.load(in);
}
return springFactories;
}
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,48 +17,29 @@
package org.springframework.boot.build.autoconfigure; package org.springframework.boot.build.autoconfigure;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import org.gradle.api.Plugin; import org.gradle.api.Plugin;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Configuration;
import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSet;
import org.springframework.boot.build.DeployedPlugin; import org.springframework.boot.build.DeployedPlugin;
import org.springframework.boot.build.architecture.ArchitectureCheck; import org.springframework.boot.build.context.properties.ConfigurationPropertiesPlugin;
import org.springframework.boot.build.architecture.ArchitecturePlugin;
/** /**
* {@link Plugin} for projects that define auto-configuration. When applied, the plugin * {@link Plugin} for projects that define auto-configuration. When applied, the plugin
* applies the {@link DeployedPlugin}. Additionally, when the {@link JavaPlugin} is * applies the {@link DeployedPlugin}. Additionally, it reacts to the presence of the
* applied it: * {@link JavaPlugin} by:
* *
* <ul> * <ul>
* <li>Adds a dependency on the auto-configuration annotation processor. * <li>Applying the {@link ConfigurationPropertiesPlugin}.
* <li>Defines a task that produces metadata describing the auto-configuration. The * <li>Adding a dependency on the auto-configuration annotation processor.
* metadata is made available as an artifact in the {@code autoConfigurationMetadata} * <li>Defining a task that produces metadata describing the auto-configuration. The
* configuration. * metadata is made available as an artifact in the
* <li>Reacts to the {@link ArchitecturePlugin} being applied and:
* <ul>
* <li>Adds a rule to the {@code checkArchitectureMain} task to verify that all
* {@code AutoConfiguration} classes are listed in the {@code AutoConfiguration.imports}
* file.
* </ul>
* </ul> * </ul>
* *
* @author Andy Wilkinson * @author Andy Wilkinson
@ -71,91 +52,29 @@ public class AutoConfigurationPlugin implements Plugin<Project> {
*/ */
public static final String AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME = "autoConfigurationMetadata"; public static final String AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME = "autoConfigurationMetadata";
private static final String AUTO_CONFIGURATION_IMPORTS_PATH = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports";
@Override @Override
public void apply(Project project) { public void apply(Project project) {
project.getPlugins().apply(DeployedPlugin.class); project.getPlugins().apply(DeployedPlugin.class);
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> { project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
project.getPlugins().apply(ConfigurationPropertiesPlugin.class);
Configuration annotationProcessors = project.getConfigurations() Configuration annotationProcessors = project.getConfigurations()
.getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME); .getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME);
annotationProcessors.getDependencies() annotationProcessors.getDependencies()
.add(project.getDependencies() .add(project.getDependencies().project(Collections.singletonMap("path",
.project(Collections.singletonMap("path",
":spring-boot-project:spring-boot-tools:spring-boot-autoconfigure-processor"))); ":spring-boot-project:spring-boot-tools:spring-boot-autoconfigure-processor")));
annotationProcessors.getDependencies() annotationProcessors.getDependencies()
.add(project.getDependencies() .add(project.getDependencies().project(Collections.singletonMap("path",
.project(Collections.singletonMap("path",
":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor"))); ":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor")));
project.getTasks().create("autoConfigurationMetadata", AutoConfigurationMetadata.class, (task) -> { project.getTasks().create("autoConfigurationMetadata", AutoConfigurationMetadata.class, (task) -> {
SourceSet main = project.getExtensions() SourceSet main = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets()
.getByType(JavaPluginExtension.class) .getByName(SourceSet.MAIN_SOURCE_SET_NAME);
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
task.setSourceSet(main); task.setSourceSet(main);
task.dependsOn(main.getClassesTaskName()); task.dependsOn(main.getClassesTaskName());
task.setOutputFile(new File(project.getBuildDir(), "auto-configuration-metadata.properties")); task.setOutputFile(new File(project.getBuildDir(), "auto-configuration-metadata.properties"));
project.getArtifacts() project.getArtifacts().add(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME,
.add(AutoConfigurationPlugin.AUTO_CONFIGURATION_METADATA_CONFIGURATION_NAME, project.provider((Callable<File>) task::getOutputFile), (artifact) -> artifact.builtBy(task));
project.provider((Callable<File>) task::getOutputFile),
(artifact) -> artifact.builtBy(task));
}); });
project.getPlugins().withType(ArchitecturePlugin.class, (architecturePlugin) -> {
project.getTasks().named("checkArchitectureMain", ArchitectureCheck.class).configure((task) -> {
SourceSet main = project.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
File resourcesDirectory = main.getOutput().getResourcesDir();
task.dependsOn(main.getProcessResourcesTaskName());
task.getInputs().files(resourcesDirectory).optional().withPathSensitivity(PathSensitivity.RELATIVE);
task.getRules()
.add(allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports(
autoConfigurationImports(project, resourcesDirectory)));
});
});
});
}
private ArchRule allClassesAnnotatedWithAutoConfigurationShouldBeListedInAutoConfigurationImports(
Provider<AutoConfigurationImports> imports) {
return ArchRuleDefinition.classes()
.that()
.areAnnotatedWith("org.springframework.boot.autoconfigure.AutoConfiguration")
.should(beListedInAutoConfigurationImports(imports))
.allowEmptyShould(true);
}
private ArchCondition<JavaClass> beListedInAutoConfigurationImports(Provider<AutoConfigurationImports> imports) {
return new ArchCondition<>("be listed in " + AUTO_CONFIGURATION_IMPORTS_PATH) {
@Override
public void check(JavaClass item, ConditionEvents events) {
AutoConfigurationImports autoConfigurationImports = imports.get();
if (!autoConfigurationImports.imports.contains(item.getName())) {
events.add(SimpleConditionEvent.violated(item,
item.getName() + " was not listed in " + autoConfigurationImports.importsFile));
}
}
};
}
private Provider<AutoConfigurationImports> autoConfigurationImports(Project project, File resourcesDirectory) {
Path importsFile = new File(resourcesDirectory, AUTO_CONFIGURATION_IMPORTS_PATH).toPath();
return project.provider(() -> {
try {
return new AutoConfigurationImports(project.getProjectDir().toPath().relativize(importsFile),
Files.readAllLines(importsFile));
}
catch (IOException ex) {
throw new RuntimeException("Failed to read AutoConfiguration.imports", ex);
}
}); });
} }
private static record AutoConfigurationImports(Path importsFile, List<String> imports) {
}
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,13 +21,12 @@ import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource; import javax.xml.transform.dom.DOMSource;
@ -48,20 +47,24 @@ import org.gradle.api.Project;
import org.gradle.api.Task; import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.JavaPlatformPlugin; import org.gradle.api.plugins.JavaPlatformPlugin;
import org.gradle.api.publish.maven.tasks.GenerateMavenPom; import org.gradle.api.publish.maven.tasks.GenerateMavenPom;
import org.gradle.api.tasks.Sync; import org.gradle.api.tasks.Sync;
import org.gradle.api.tasks.TaskExecutionException; import org.gradle.api.tasks.TaskExecutionException;
import org.gradle.util.ConfigureUtil;
import org.w3c.dom.Document; import org.w3c.dom.Document;
import org.w3c.dom.NodeList; import org.w3c.dom.NodeList;
import org.springframework.boot.build.DeployedPlugin; import org.springframework.boot.build.DeployedPlugin;
import org.springframework.boot.build.bom.Library.DependencyConstraintsDependencyVersions;
import org.springframework.boot.build.bom.Library.DependencyLockDependencyVersions;
import org.springframework.boot.build.bom.Library.DependencyVersions;
import org.springframework.boot.build.bom.Library.Exclusion; import org.springframework.boot.build.bom.Library.Exclusion;
import org.springframework.boot.build.bom.Library.Group; import org.springframework.boot.build.bom.Library.Group;
import org.springframework.boot.build.bom.Library.LibraryVersion; import org.springframework.boot.build.bom.Library.LibraryVersion;
import org.springframework.boot.build.bom.Library.Module; import org.springframework.boot.build.bom.Library.Module;
import org.springframework.boot.build.bom.Library.ProhibitedVersion; import org.springframework.boot.build.bom.Library.ProhibitedVersion;
import org.springframework.boot.build.bom.Library.VersionAlignment;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion; import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
import org.springframework.boot.build.mavenplugin.MavenExec; import org.springframework.boot.build.mavenplugin.MavenExec;
import org.springframework.util.FileCopyUtils; import org.springframework.util.FileCopyUtils;
@ -79,7 +82,7 @@ public class BomExtension {
private final List<Library> libraries = new ArrayList<>(); private final List<Library> libraries = new ArrayList<>();
private final UpgradeHandler upgradeHandler; private final UpgradeHandler upgradeHandler = new UpgradeHandler();
private final DependencyHandler dependencyHandler; private final DependencyHandler dependencyHandler;
@ -87,7 +90,6 @@ public class BomExtension {
public BomExtension(DependencyHandler dependencyHandler, Project project) { public BomExtension(DependencyHandler dependencyHandler, Project project) {
this.dependencyHandler = dependencyHandler; this.dependencyHandler = dependencyHandler;
this.upgradeHandler = project.getObjects().newInstance(UpgradeHandler.class);
this.project = project; this.project = project;
} }
@ -95,8 +97,8 @@ public class BomExtension {
return this.libraries; return this.libraries;
} }
public void upgrade(Action<UpgradeHandler> action) { public void upgrade(Closure<?> closure) {
action.execute(this.upgradeHandler); ConfigureUtil.configure(closure, this.upgradeHandler);
} }
public Upgrade getUpgrade() { public Upgrade getUpgrade() {
@ -104,56 +106,54 @@ public class BomExtension {
this.upgradeHandler.gitHub.repository, this.upgradeHandler.gitHub.issueLabels)); this.upgradeHandler.gitHub.repository, this.upgradeHandler.gitHub.issueLabels));
} }
public void library(String name, Action<LibraryHandler> action) { public void library(String name, Closure<?> closure) {
library(name, null, action); this.library(name, null, closure);
} }
public void library(String name, String version, Action<LibraryHandler> action) { public void library(String name, String version, Closure<?> closure) {
ObjectFactory objects = this.project.getObjects(); LibraryHandler libraryHandler = new LibraryHandler(version);
LibraryHandler libraryHandler = objects.newInstance(LibraryHandler.class, (version != null) ? version : ""); ConfigureUtil.configure(closure, libraryHandler);
action.execute(libraryHandler); LibraryVersion libraryVersion = new LibraryVersion(DependencyVersion.parse(libraryHandler.version),
LibraryVersion libraryVersion = new LibraryVersion(DependencyVersion.parse(libraryHandler.version)); libraryHandler.versionAlignment);
addLibrary(new Library(name, libraryHandler.calendarName, libraryVersion, libraryHandler.groups, addLibrary(new Library(name, libraryVersion, libraryHandler.groups, libraryHandler.prohibitedVersions,
libraryHandler.prohibitedVersions, libraryHandler.considerSnapshots)); libraryHandler.dependencyVersions));
} }
public void effectiveBomArtifact() { public void effectiveBomArtifact() {
Configuration effectiveBomConfiguration = this.project.getConfigurations().create("effectiveBom"); Configuration effectiveBomConfiguration = this.project.getConfigurations().create("effectiveBom");
this.project.getTasks() this.project.getTasks().matching((task) -> task.getName().equals(DeployedPlugin.GENERATE_POM_TASK_NAME))
.matching((task) -> task.getName().equals(DeployedPlugin.GENERATE_POM_TASK_NAME)) .all((task) -> {
.all((task) -> { Sync syncBom = this.project.getTasks().create("syncBom", Sync.class);
Sync syncBom = this.project.getTasks().create("syncBom", Sync.class); syncBom.dependsOn(task);
syncBom.dependsOn(task); File generatedBomDir = new File(this.project.getBuildDir(), "generated/bom");
File generatedBomDir = new File(this.project.getBuildDir(), "generated/bom"); syncBom.setDestinationDir(generatedBomDir);
syncBom.setDestinationDir(generatedBomDir); syncBom.from(((GenerateMavenPom) task).getDestination(), (pom) -> pom.rename((name) -> "pom.xml"));
syncBom.from(((GenerateMavenPom) task).getDestination(), (pom) -> pom.rename((name) -> "pom.xml")); try {
try { String settingsXmlContent = FileCopyUtils
String settingsXmlContent = FileCopyUtils .copyToString(new InputStreamReader(
.copyToString(new InputStreamReader( getClass().getClassLoader().getResourceAsStream("effective-bom-settings.xml"),
getClass().getClassLoader().getResourceAsStream("effective-bom-settings.xml"), StandardCharsets.UTF_8))
StandardCharsets.UTF_8)) .replace("localRepositoryPath",
.replace("localRepositoryPath", new File(this.project.getBuildDir(), "local-m2-repository").getAbsolutePath());
new File(this.project.getBuildDir(), "local-m2-repository").getAbsolutePath()); syncBom.from(this.project.getResources().getText().fromString(settingsXmlContent),
syncBom.from(this.project.getResources().getText().fromString(settingsXmlContent), (settingsXml) -> settingsXml.rename((name) -> "settings.xml"));
(settingsXml) -> settingsXml.rename((name) -> "settings.xml")); }
} catch (IOException ex) {
catch (IOException ex) { throw new GradleException("Failed to prepare settings.xml", ex);
throw new GradleException("Failed to prepare settings.xml", ex); }
} MavenExec generateEffectiveBom = this.project.getTasks().create("generateEffectiveBom",
MavenExec generateEffectiveBom = this.project.getTasks() MavenExec.class);
.create("generateEffectiveBom", MavenExec.class); generateEffectiveBom.setProjectDir(generatedBomDir);
generateEffectiveBom.setProjectDir(generatedBomDir); File effectiveBom = new File(this.project.getBuildDir(),
File effectiveBom = new File(this.project.getBuildDir(), "generated/effective-bom/" + this.project.getName() + "-effective-bom.xml");
"generated/effective-bom/" + this.project.getName() + "-effective-bom.xml"); generateEffectiveBom.args("--settings", "settings.xml", "help:effective-pom",
generateEffectiveBom.args("--settings", "settings.xml", "help:effective-pom", "-Doutput=" + effectiveBom);
"-Doutput=" + effectiveBom); generateEffectiveBom.dependsOn(syncBom);
generateEffectiveBom.dependsOn(syncBom); generateEffectiveBom.getOutputs().file(effectiveBom);
generateEffectiveBom.getOutputs().file(effectiveBom); generateEffectiveBom.doLast(new StripUnrepeatableOutputAction(effectiveBom));
generateEffectiveBom.doLast(new StripUnrepeatableOutputAction(effectiveBom)); this.project.getArtifacts().add(effectiveBomConfiguration.getName(), effectiveBom,
this.project.getArtifacts()
.add(effectiveBomConfiguration.getName(), effectiveBom,
(artifact) -> artifact.builtBy(generateEffectiveBom)); (artifact) -> artifact.builtBy(generateEffectiveBom));
}); });
} }
private String createDependencyNotation(String groupId, String artifactId, DependencyVersion version) { private String createDependencyNotation(String groupId, String artifactId, DependencyVersion version) {
@ -164,18 +164,13 @@ public class BomExtension {
return this.properties; return this.properties;
} }
String getArtifactVersionProperty(String groupId, String artifactId, String classifier) { String getArtifactVersionProperty(String groupId, String artifactId) {
String coordinates = groupId + ":" + artifactId + ":" + classifier; String coordinates = groupId + ":" + artifactId;
return this.artifactVersionProperties.get(coordinates); return this.artifactVersionProperties.get(coordinates);
} }
private void putArtifactVersionProperty(String groupId, String artifactId, String versionProperty) { private void putArtifactVersionProperty(String groupId, String artifactId, String versionProperty) {
putArtifactVersionProperty(groupId, artifactId, null, versionProperty); String coordinates = groupId + ":" + artifactId;
}
private void putArtifactVersionProperty(String groupId, String artifactId, String classifier,
String versionProperty) {
String coordinates = groupId + ":" + artifactId + ":" + ((classifier != null) ? classifier : "");
String existing = this.artifactVersionProperties.putIfAbsent(coordinates, versionProperty); String existing = this.artifactVersionProperties.putIfAbsent(coordinates, versionProperty);
if (existing != null) { if (existing != null) {
throw new InvalidUserDataException("Cannot put version property for '" + coordinates throw new InvalidUserDataException("Cannot put version property for '" + coordinates
@ -191,10 +186,9 @@ public class BomExtension {
} }
for (Group group : library.getGroups()) { for (Group group : library.getGroups()) {
for (Module module : group.getModules()) { for (Module module : group.getModules()) {
putArtifactVersionProperty(group.getId(), module.getName(), module.getClassifier(), versionProperty); putArtifactVersionProperty(group.getId(), module.getName(), versionProperty);
this.dependencyHandler.getConstraints() this.dependencyHandler.getConstraints().add(JavaPlatformPlugin.API_CONFIGURATION_NAME,
.add(JavaPlatformPlugin.API_CONFIGURATION_NAME, createDependencyNotation(group.getId(), createDependencyNotation(group.getId(), module.getName(), library.getVersion().getVersion()));
module.getName(), library.getVersion().getVersion()));
} }
for (String bomImport : group.getBoms()) { for (String bomImport : group.getBoms()) {
putArtifactVersionProperty(group.getId(), bomImport, versionProperty); putArtifactVersionProperty(group.getId(), bomImport, versionProperty);
@ -214,87 +208,60 @@ public class BomExtension {
private final List<ProhibitedVersion> prohibitedVersions = new ArrayList<>(); private final List<ProhibitedVersion> prohibitedVersions = new ArrayList<>();
private boolean considerSnapshots = false;
private String version; private String version;
private String calendarName; private VersionAlignment versionAlignment;
private DependencyVersions dependencyVersions;
@Inject
public LibraryHandler(String version) { public LibraryHandler(String version) {
this.version = version; this.version = version;
} }
public void version(String version) { public void version(String version, Closure<?> closure) {
this.version = version; this.version = version;
VersionHandler versionHandler = new VersionHandler();
ConfigureUtil.configure(closure, versionHandler);
this.versionAlignment = new VersionAlignment(versionHandler.libraryName);
} }
public void considerSnapshots() { public void group(String id, Closure<?> closure) {
this.considerSnapshots = true;
}
public void setCalendarName(String calendarName) {
this.calendarName = calendarName;
}
public void group(String id, Action<GroupHandler> action) {
GroupHandler groupHandler = new GroupHandler(id); GroupHandler groupHandler = new GroupHandler(id);
action.execute(groupHandler); ConfigureUtil.configure(closure, groupHandler);
this.groups this.groups
.add(new Group(groupHandler.id, groupHandler.modules, groupHandler.plugins, groupHandler.imports)); .add(new Group(groupHandler.id, groupHandler.modules, groupHandler.plugins, groupHandler.imports));
} }
public void prohibit(Action<ProhibitedHandler> action) { public void prohibit(String range, Closure<?> closure) {
ProhibitedHandler handler = new ProhibitedHandler(); ProhibitedVersionHandler prohibitedVersionHandler = new ProhibitedVersionHandler();
action.execute(handler); ConfigureUtil.configure(closure, prohibitedVersionHandler);
this.prohibitedVersions.add(new ProhibitedVersion(handler.versionRange, handler.startsWith, try {
handler.endsWith, handler.contains, handler.reason)); this.prohibitedVersions.add(new ProhibitedVersion(VersionRange.createFromVersionSpec(range),
prohibitedVersionHandler.reason));
}
catch (InvalidVersionSpecificationException ex) {
throw new InvalidUserCodeException("Invalid version range", ex);
}
} }
public static class ProhibitedHandler { public void dependencyVersions(Closure<?> closure) {
DependencyVersionsHandler dependencyVersionsHandler = new DependencyVersionsHandler();
private String reason; ConfigureUtil.configure(closure, dependencyVersionsHandler);
}
private final List<String> startsWith = new ArrayList<>();
private final List<String> endsWith = new ArrayList<>();
private final List<String> contains = new ArrayList<>();
private VersionRange versionRange;
public void versionRange(String versionRange) {
try {
this.versionRange = VersionRange.createFromVersionSpec(versionRange);
}
catch (InvalidVersionSpecificationException ex) {
throw new InvalidUserCodeException("Invalid version range", ex);
}
}
public void startsWith(String startsWith) { public static class VersionHandler {
this.startsWith.add(startsWith);
}
public void startsWith(Collection<String> startsWith) { private String libraryName;
this.startsWith.addAll(startsWith);
}
public void endsWith(String endsWith) { public void shouldAlignWithVersionFrom(String libraryName) {
this.endsWith.add(endsWith); this.libraryName = libraryName;
} }
public void endsWith(Collection<String> endsWith) { }
this.endsWith.addAll(endsWith);
}
public void contains(String contains) { public static class ProhibitedVersionHandler {
this.contains.add(contains);
}
public void contains(List<String> contains) { private String reason;
this.contains.addAll(contains);
}
public void because(String because) { public void because(String because) {
this.reason = because; this.reason = because;
@ -318,8 +285,8 @@ public class BomExtension {
public void setModules(List<Object> modules) { public void setModules(List<Object> modules) {
this.modules = modules.stream() this.modules = modules.stream()
.map((input) -> (input instanceof Module module) ? module : new Module((String) input)) .map((input) -> (input instanceof Module) ? (Module) input : new Module((String) input))
.toList(); .collect(Collectors.toList());
} }
public void setImports(List<String> imports) { public void setImports(List<String> imports) {
@ -333,35 +300,44 @@ public class BomExtension {
public Object methodMissing(String name, Object args) { public Object methodMissing(String name, Object args) {
if (args instanceof Object[] && ((Object[]) args).length == 1) { if (args instanceof Object[] && ((Object[]) args).length == 1) {
Object arg = ((Object[]) args)[0]; Object arg = ((Object[]) args)[0];
if (arg instanceof Closure<?> closure) { if (arg instanceof Closure) {
ModuleHandler moduleHandler = new ModuleHandler(); ExclusionHandler exclusionHandler = new ExclusionHandler();
closure.setResolveStrategy(Closure.DELEGATE_FIRST); ConfigureUtil.configure((Closure<?>) arg, exclusionHandler);
closure.setDelegate(moduleHandler); return new Module(name, exclusionHandler.exclusions);
closure.call(moduleHandler);
return new Module(name, moduleHandler.type, moduleHandler.classifier, moduleHandler.exclusions);
} }
} }
throw new InvalidUserDataException("Invalid configuration for module '" + name + "'"); throw new InvalidUserDataException("Invalid exclusion configuration for module '" + name + "'");
} }
public class ModuleHandler { public class ExclusionHandler {
private final List<Exclusion> exclusions = new ArrayList<>(); private final List<Exclusion> exclusions = new ArrayList<>();
private String type;
private String classifier;
public void exclude(Map<String, String> exclusion) { public void exclude(Map<String, String> exclusion) {
this.exclusions.add(new Exclusion(exclusion.get("group"), exclusion.get("module"))); this.exclusions.add(new Exclusion(exclusion.get("group"), exclusion.get("module")));
} }
public void setType(String type) { }
this.type = type;
}
public class DependencyVersionsHandler {
public void extractFrom(Closure<?> closure) {
ExtractFromHandler extractFromHandler = new ExtractFromHandler();
ConfigureUtil.configure(closure, extractFromHandler);
}
public class ExtractFromHandler {
public void dependencyLock(String location) {
LibraryHandler.this.dependencyVersions = new DependencyLockDependencyVersions(location,
LibraryHandler.this.version);
} }
public void setClassifier(String classifier) { public void dependencyConstraints(String location) {
this.classifier = classifier; LibraryHandler.this.dependencyVersions = new DependencyConstraintsDependencyVersions(location,
LibraryHandler.this.version);
} }
} }
@ -380,8 +356,8 @@ public class BomExtension {
this.upgradePolicy = upgradePolicy; this.upgradePolicy = upgradePolicy;
} }
public void gitHub(Action<GitHubHandler> action) { public void gitHub(Closure<?> closure) {
action.execute(this.gitHub); ConfigureUtil.configure(closure, this.gitHub);
} }
} }
@ -481,9 +457,8 @@ public class BomExtension {
org.w3c.dom.Node reporting = (org.w3c.dom.Node) xpath.evaluate("/project/reporting", document, org.w3c.dom.Node reporting = (org.w3c.dom.Node) xpath.evaluate("/project/reporting", document,
XPathConstants.NODE); XPathConstants.NODE);
reporting.getParentNode().removeChild(reporting); reporting.getParentNode().removeChild(reporting);
TransformerFactory.newInstance() TransformerFactory.newInstance().newTransformer().transform(new DOMSource(document),
.newTransformer() new StreamResult(this.effectiveBom));
.transform(new DOMSource(document), new StreamResult(this.effectiveBom));
} }
catch (Exception ex) { catch (Exception ex) {
throw new TaskExecutionException(task, ex); throw new TaskExecutionException(task, ex);

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,12 +17,10 @@
package org.springframework.boot.build.bom; package org.springframework.boot.build.bom;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import groovy.namespace.QName;
import groovy.util.Node; import groovy.util.Node;
import groovy.xml.QName;
import org.gradle.api.Plugin; import org.gradle.api.Plugin;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Configuration;
@ -36,8 +34,6 @@ import org.gradle.api.publish.maven.MavenPublication;
import org.springframework.boot.build.DeployedPlugin; import org.springframework.boot.build.DeployedPlugin;
import org.springframework.boot.build.MavenRepositoryPlugin; import org.springframework.boot.build.MavenRepositoryPlugin;
import org.springframework.boot.build.bom.Library.Group; import org.springframework.boot.build.bom.Library.Group;
import org.springframework.boot.build.bom.Library.Module;
import org.springframework.boot.build.bom.bomr.MoveToSnapshots;
import org.springframework.boot.build.bom.bomr.UpgradeBom; import org.springframework.boot.build.bom.bomr.UpgradeBom;
/** /**
@ -60,28 +56,25 @@ public class BomPlugin implements Plugin<Project> {
JavaPlatformExtension javaPlatform = project.getExtensions().getByType(JavaPlatformExtension.class); JavaPlatformExtension javaPlatform = project.getExtensions().getByType(JavaPlatformExtension.class);
javaPlatform.allowDependencies(); javaPlatform.allowDependencies();
createApiEnforcedConfiguration(project); createApiEnforcedConfiguration(project);
BomExtension bom = project.getExtensions() BomExtension bom = project.getExtensions().create("bom", BomExtension.class, project.getDependencies(),
.create("bom", BomExtension.class, project.getDependencies(), project); project);
project.getTasks().create("bomrCheck", CheckBom.class, bom); project.getTasks().create("bomrCheck", CheckBom.class, bom);
project.getTasks().create("bomrUpgrade", UpgradeBom.class, bom); project.getTasks().create("bomrUpgrade", UpgradeBom.class, bom);
project.getTasks().create("moveToSnapshots", MoveToSnapshots.class, bom);
new PublishingCustomizer(project, bom).customize(); new PublishingCustomizer(project, bom).customize();
} }
private void createApiEnforcedConfiguration(Project project) { private void createApiEnforcedConfiguration(Project project) {
Configuration apiEnforced = project.getConfigurations() Configuration apiEnforced = project.getConfigurations().create(API_ENFORCED_CONFIGURATION_NAME,
.create(API_ENFORCED_CONFIGURATION_NAME, (configuration) -> { (configuration) -> {
configuration.setCanBeConsumed(false); configuration.setCanBeConsumed(false);
configuration.setCanBeResolved(false); configuration.setCanBeResolved(false);
configuration.setVisible(false); configuration.setVisible(false);
}); });
project.getConfigurations() project.getConfigurations().getByName(JavaPlatformPlugin.ENFORCED_API_ELEMENTS_CONFIGURATION_NAME)
.getByName(JavaPlatformPlugin.ENFORCED_API_ELEMENTS_CONFIGURATION_NAME) .extendsFrom(apiEnforced);
.extendsFrom(apiEnforced); project.getConfigurations().getByName(JavaPlatformPlugin.ENFORCED_RUNTIME_ELEMENTS_CONFIGURATION_NAME)
project.getConfigurations() .extendsFrom(apiEnforced);
.getByName(JavaPlatformPlugin.ENFORCED_RUNTIME_ELEMENTS_CONFIGURATION_NAME)
.extendsFrom(apiEnforced);
} }
private static final class PublishingCustomizer { private static final class PublishingCustomizer {
@ -113,10 +106,8 @@ public class BomPlugin implements Plugin<Project> {
Node dependencyManagement = findChild(projectNode, "dependencyManagement"); Node dependencyManagement = findChild(projectNode, "dependencyManagement");
if (dependencyManagement != null) { if (dependencyManagement != null) {
addPropertiesBeforeDependencyManagement(projectNode, properties); addPropertiesBeforeDependencyManagement(projectNode, properties);
addClassifiedManagedDependencies(dependencyManagement);
replaceVersionsWithVersionPropertyReferences(dependencyManagement); replaceVersionsWithVersionPropertyReferences(dependencyManagement);
addExclusionsToManagedDependencies(dependencyManagement); addExclusionsToManagedDependencies(dependencyManagement);
addTypesToManagedDependencies(dependencyManagement);
} }
else { else {
projectNode.children().add(properties); projectNode.children().add(properties);
@ -141,9 +132,7 @@ public class BomPlugin implements Plugin<Project> {
for (Node dependency : findChildren(dependencies, "dependency")) { for (Node dependency : findChildren(dependencies, "dependency")) {
String groupId = findChild(dependency, "groupId").text(); String groupId = findChild(dependency, "groupId").text();
String artifactId = findChild(dependency, "artifactId").text(); String artifactId = findChild(dependency, "artifactId").text();
Node classifierNode = findChild(dependency, "classifier"); String versionProperty = this.bom.getArtifactVersionProperty(groupId, artifactId);
String classifier = (classifierNode != null) ? classifierNode.text() : "";
String versionProperty = this.bom.getArtifactVersionProperty(groupId, artifactId, classifier);
if (versionProperty != null) { if (versionProperty != null) {
findChild(dependency, "version").setValue("${" + versionProperty + "}"); findChild(dependency, "version").setValue("${" + versionProperty + "}");
} }
@ -157,82 +146,16 @@ public class BomPlugin implements Plugin<Project> {
for (Node dependency : findChildren(dependencies, "dependency")) { for (Node dependency : findChildren(dependencies, "dependency")) {
String groupId = findChild(dependency, "groupId").text(); String groupId = findChild(dependency, "groupId").text();
String artifactId = findChild(dependency, "artifactId").text(); String artifactId = findChild(dependency, "artifactId").text();
this.bom.getLibraries() this.bom.getLibraries().stream().flatMap((library) -> library.getGroups().stream())
.stream() .filter((group) -> group.getId().equals(groupId))
.flatMap((library) -> library.getGroups().stream()) .flatMap((group) -> group.getModules().stream())
.filter((group) -> group.getId().equals(groupId)) .filter((module) -> module.getName().equals(artifactId))
.flatMap((group) -> group.getModules().stream()) .flatMap((module) -> module.getExclusions().stream()).forEach((exclusion) -> {
.filter((module) -> module.getName().equals(artifactId)) Node exclusions = findOrCreateNode(dependency, "exclusions");
.flatMap((module) -> module.getExclusions().stream()) Node node = new Node(exclusions, "exclusion");
.forEach((exclusion) -> { node.appendNode("groupId", exclusion.getGroupId());
Node exclusions = findOrCreateNode(dependency, "exclusions"); node.appendNode("artifactId", exclusion.getArtifactId());
Node node = new Node(exclusions, "exclusion"); });
node.appendNode("groupId", exclusion.getGroupId());
node.appendNode("artifactId", exclusion.getArtifactId());
});
}
}
}
private void addTypesToManagedDependencies(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();
Set<String> types = 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))
.map(Module::getType)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (types.size() > 1) {
throw new IllegalStateException(
"Multiple types for " + groupId + ":" + artifactId + ": " + types);
}
if (types.size() == 1) {
String type = types.iterator().next();
dependency.appendNode("type", type);
}
}
}
}
@SuppressWarnings("unchecked")
private void addClassifiedManagedDependencies(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();
String version = findChild(dependency, "version").text();
Set<String> classifiers = 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))
.map(Module::getClassifier)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Node target = dependency;
for (String classifier : classifiers) {
if (classifier.length() > 0) {
if (target == null) {
target = new Node(null, "dependency");
target.appendNode("groupId", groupId);
target.appendNode("artifactId", artifactId);
target.appendNode("version", version);
int index = dependency.parent().children().indexOf(dependency);
dependency.parent().children().add(index + 1, target);
}
target.appendNode("classifier", classifier);
}
target = null;
}
} }
} }
} }
@ -268,8 +191,9 @@ public class BomPlugin implements Plugin<Project> {
private Node findChild(Node parent, String name) { private Node findChild(Node parent, String name) {
for (Object child : parent.children()) { for (Object child : parent.children()) {
if (child instanceof Node node) { if (child instanceof Node) {
if ((node.name() instanceof QName qname) && name.equals(qname.getLocalPart())) { Node node = (Node) child;
if ((node.name() instanceof QName) && name.equals(((QName) node.name()).getLocalPart())) {
return node; return node;
} }
if (name.equals(node.name())) { if (name.equals(node.name())) {
@ -282,12 +206,15 @@ public class BomPlugin implements Plugin<Project> {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private List<Node> findChildren(Node parent, String name) { private List<Node> findChildren(Node parent, String name) {
return parent.children().stream().filter((child) -> isNodeWithName(child, name)).toList(); return (List<Node>) parent.children().stream().filter((child) -> isNodeWithName(child, name))
.collect(Collectors.toList());
} }
private boolean isNodeWithName(Object candidate, String name) { private boolean isNodeWithName(Object candidate, String name) {
if (candidate instanceof Node node) { if (candidate instanceof Node) {
if ((node.name() instanceof QName qname) && name.equals(qname.getLocalPart())) { Node node = (Node) candidate;
if ((node.name() instanceof QName) && name.equals(((QName) node.name()).getLocalPart())) {
return true; return true;
} }
if (name.equals(node.name())) { if (name.equals(node.name())) {

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -59,18 +59,14 @@ public class CheckBom extends DefaultTask {
private void checkExclusions(String groupId, Module module, DependencyVersion version) { private void checkExclusions(String groupId, Module module, DependencyVersion version) {
Set<String> resolved = getProject().getConfigurations() Set<String> resolved = getProject().getConfigurations()
.detachedConfiguration( .detachedConfiguration(
getProject().getDependencies().create(groupId + ":" + module.getName() + ":" + version)) getProject().getDependencies().create(groupId + ":" + module.getName() + ":" + version))
.getResolvedConfiguration() .getResolvedConfiguration().getResolvedArtifacts().stream()
.getResolvedArtifacts() .map((artifact) -> artifact.getModuleVersion().getId())
.stream() .map((id) -> id.getGroup() + ":" + id.getModule().getName()).collect(Collectors.toSet());
.map((artifact) -> artifact.getModuleVersion().getId()) Set<String> exclusions = module.getExclusions().stream()
.map((id) -> id.getGroup() + ":" + id.getModule().getName()) .map((exclusion) -> exclusion.getGroupId() + ":" + exclusion.getArtifactId())
.collect(Collectors.toSet()); .collect(Collectors.toSet());
Set<String> exclusions = module.getExclusions()
.stream()
.map((exclusion) -> exclusion.getGroupId() + ":" + exclusion.getArtifactId())
.collect(Collectors.toSet());
Set<String> unused = new TreeSet<>(); Set<String> unused = new TreeSet<>();
for (String exclusion : exclusions) { for (String exclusion : exclusions) {
if (!resolved.contains(exclusion)) { if (!resolved.contains(exclusion)) {

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,11 +16,20 @@
package org.springframework.boot.build.bom; package org.springframework.boot.build.bom;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.maven.artifact.versioning.VersionRange; import org.apache.maven.artifact.versioning.VersionRange;
import org.gradle.api.GradleException;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion; import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
@ -34,8 +43,6 @@ public class Library {
private final String name; private final String name;
private final String calendarName;
private final LibraryVersion version; private final LibraryVersion version;
private final List<Group> groups; private final List<Group> groups;
@ -44,39 +51,32 @@ public class Library {
private final List<ProhibitedVersion> prohibitedVersions; private final List<ProhibitedVersion> prohibitedVersions;
private final boolean considerSnapshots; private final DependencyVersions dependencyVersions;
/** /**
* Create a new {@code Library} with the given {@code name}, {@code version}, and * Create a new {@code Library} with the given {@code name}, {@code version}, and
* {@code groups}. * {@code groups}.
* @param name name of the library * @param name name of the library
* @param calendarName name of the library as it appears in the Spring Calendar. May
* be {@code null} in which case the {@code name} is used.
* @param version version of the library * @param version version of the library
* @param groups groups in the library * @param groups groups in the library
* @param prohibitedVersions version of the library that are prohibited * @param prohibitedVersions version of the library that are prohibited
* @param considerSnapshots whether to consider snapshots * @param dependencyVersions the library's dependency versions
*/ */
public Library(String name, String calendarName, LibraryVersion version, List<Group> groups, public Library(String name, LibraryVersion version, List<Group> groups, List<ProhibitedVersion> prohibitedVersions,
List<ProhibitedVersion> prohibitedVersions, boolean considerSnapshots) { DependencyVersions dependencyVersions) {
this.name = name; this.name = name;
this.calendarName = (calendarName != null) ? calendarName : name;
this.version = version; this.version = version;
this.groups = groups; this.groups = groups;
this.versionProperty = "Spring Boot".equals(name) ? null this.versionProperty = "Spring Boot".equals(name) ? null
: name.toLowerCase(Locale.ENGLISH).replace(' ', '-') + ".version"; : name.toLowerCase(Locale.ENGLISH).replace(' ', '-') + ".version";
this.prohibitedVersions = prohibitedVersions; this.prohibitedVersions = prohibitedVersions;
this.considerSnapshots = considerSnapshots; this.dependencyVersions = dependencyVersions;
} }
public String getName() { public String getName() {
return this.name; return this.name;
} }
public String getCalendarName() {
return this.calendarName;
}
public LibraryVersion getVersion() { public LibraryVersion getVersion() {
return this.version; return this.version;
} }
@ -93,8 +93,8 @@ public class Library {
return this.prohibitedVersions; return this.prohibitedVersions;
} }
public boolean isConsiderSnapshots() { public DependencyVersions getDependencyVersions() {
return this.considerSnapshots; return this.dependencyVersions;
} }
/** /**
@ -104,20 +104,10 @@ public class Library {
private final VersionRange range; private final VersionRange range;
private final List<String> startsWith;
private final List<String> endsWith;
private final List<String> contains;
private final String reason; private final String reason;
public ProhibitedVersion(VersionRange range, List<String> startsWith, List<String> endsWith, public ProhibitedVersion(VersionRange range, String reason) {
List<String> contains, String reason) {
this.range = range; this.range = range;
this.startsWith = startsWith;
this.endsWith = endsWith;
this.contains = contains;
this.reason = reason; this.reason = reason;
} }
@ -125,18 +115,6 @@ public class Library {
return this.range; return this.range;
} }
public List<String> getStartsWith() {
return this.startsWith;
}
public List<String> getEndsWith() {
return this.endsWith;
}
public List<String> getContains() {
return this.contains;
}
public String getReason() { public String getReason() {
return this.reason; return this.reason;
} }
@ -147,14 +125,21 @@ public class Library {
private final DependencyVersion version; private final DependencyVersion version;
public LibraryVersion(DependencyVersion version) { private final VersionAlignment versionAlignment;
public LibraryVersion(DependencyVersion version, VersionAlignment versionAlignment) {
this.version = version; this.version = version;
this.versionAlignment = versionAlignment;
} }
public DependencyVersion getVersion() { public DependencyVersion getVersion() {
return this.version; return this.version;
} }
public VersionAlignment getVersionAlignment() {
return this.versionAlignment;
}
} }
/** /**
@ -202,28 +187,14 @@ public class Library {
private final String name; private final String name;
private final String type;
private final String classifier;
private final List<Exclusion> exclusions; private final List<Exclusion> exclusions;
public Module(String name) { public Module(String name) {
this(name, Collections.emptyList()); this(name, Collections.emptyList());
} }
public Module(String name, String type) {
this(name, type, null, Collections.emptyList());
}
public Module(String name, List<Exclusion> exclusions) { public Module(String name, List<Exclusion> exclusions) {
this(name, null, null, exclusions);
}
public Module(String name, String type, String classifier, List<Exclusion> exclusions) {
this.name = name; this.name = name;
this.type = type;
this.classifier = (classifier != null) ? classifier : "";
this.exclusions = exclusions; this.exclusions = exclusions;
} }
@ -231,14 +202,6 @@ public class Library {
return this.name; return this.name;
} }
public String getClassifier() {
return this.classifier;
}
public String getType() {
return this.type;
}
public List<Exclusion> getExclusions() { public List<Exclusion> getExclusions() {
return this.exclusions; return this.exclusions;
} }
@ -269,4 +232,128 @@ public class Library {
} }
public interface DependencyVersions {
String getVersion(String groupId, String artifactId);
default boolean available() {
return true;
}
}
public static class DependencyLockDependencyVersions implements DependencyVersions {
private final Map<String, Map<String, String>> dependencyVersions = new HashMap<>();
private final String sourceTemplate;
private final String libraryVersion;
public DependencyLockDependencyVersions(String sourceTemplate, String libraryVersion) {
this.sourceTemplate = sourceTemplate;
this.libraryVersion = libraryVersion;
}
@Override
public boolean available() {
return !this.libraryVersion.contains("-SNAPSHOT");
}
@Override
public String getVersion(String groupId, String artifactId) {
if (this.dependencyVersions.isEmpty()) {
loadVersions();
}
return this.dependencyVersions.computeIfAbsent(groupId, (key) -> Collections.emptyMap()).get(artifactId);
}
private void loadVersions() {
String source = this.sourceTemplate.replace("<libraryVersion>", this.libraryVersion);
try {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(URI.create(source).toURL().openStream()))) {
String line;
while ((line = reader.readLine()) != null) {
if (!line.startsWith("#")) {
String[] components = line.split(":");
Map<String, String> groupDependencies = this.dependencyVersions
.computeIfAbsent(components[0], (key) -> new HashMap<>());
groupDependencies.put(components[1], components[2]);
}
}
}
}
catch (IOException ex) {
throw new GradleException("Failed to load versions from dependency lock file '" + source + "'", ex);
}
}
}
public static class DependencyConstraintsDependencyVersions implements DependencyVersions {
private static final Pattern CONSTRAINT_PATTERN = Pattern.compile("api \"(.+):(.+):(.+)\"");
private final Map<String, Map<String, String>> dependencyVersions = new HashMap<>();
private final String sourceTemplate;
private final String libraryVersion;
public DependencyConstraintsDependencyVersions(String sourceTemplate, String libraryVersion) {
this.sourceTemplate = sourceTemplate;
this.libraryVersion = libraryVersion;
}
@Override
public String getVersion(String groupId, String artifactId) {
if (this.dependencyVersions.isEmpty()) {
loadVersions();
}
return this.dependencyVersions.computeIfAbsent(groupId, (key) -> Collections.emptyMap()).get(artifactId);
}
private void loadVersions() {
String version = this.libraryVersion;
if (version.endsWith("-SNAPSHOT")) {
version = version.substring(0, version.lastIndexOf('.')) + ".x";
}
String source = this.sourceTemplate.replace("<libraryVersion>", version);
try {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(URI.create(source).toURL().openStream()))) {
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher = CONSTRAINT_PATTERN.matcher(line.trim());
if (matcher.matches()) {
Map<String, String> groupDependencies = this.dependencyVersions
.computeIfAbsent(matcher.group(1), (key) -> new HashMap<>());
groupDependencies.put(matcher.group(2), matcher.group(3));
}
}
}
}
catch (IOException ex) {
throw new GradleException(
"Failed to load versions from dependency constraints declared in '" + source + "'", ex);
}
}
}
public static class VersionAlignment {
private final String libraryName;
public VersionAlignment(String libraryName) {
this.libraryName = libraryName;
}
public String getLibraryName() {
return this.libraryName;
}
}
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,19 +28,24 @@ import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
public enum UpgradePolicy implements BiPredicate<DependencyVersion, DependencyVersion> { public enum UpgradePolicy implements BiPredicate<DependencyVersion, DependencyVersion> {
/** /**
* Any version. * All versions more recent than the current version will be suggested as possible
* upgrades.
*/ */
ANY((candidate, current) -> true), ANY((candidate, current) -> current.compareTo(candidate) < 0),
/** /**
* Minor versions of the current major version. * 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((candidate, current) -> candidate.isSameMajor(current)), SAME_MAJOR_VERSION(DependencyVersion::isSameMajorAndNewerThan),
/** /**
* Patch versions of the current minor version. * 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((candidate, current) -> candidate.isSameMinor(current)); SAME_MINOR_VERSION(DependencyVersion::isSameMinorAndNewerThan);
private final BiPredicate<DependencyVersion, DependencyVersion> delegate; private final BiPredicate<DependencyVersion, DependencyVersion> delegate;

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,15 +16,31 @@
package org.springframework.boot.build.bom.bomr; package org.springframework.boot.build.bom.bomr;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Set;
import java.util.SortedSet;
import java.util.stream.Collectors;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.internal.tasks.userinput.UserInputHandler; import org.gradle.api.internal.tasks.userinput.UserInputHandler;
import org.springframework.boot.build.bom.Library; import org.springframework.boot.build.bom.Library;
import org.springframework.boot.build.bom.Library.DependencyVersions;
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.Library.VersionAlignment;
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 * Interactive {@link UpgradeResolver} that uses command line input to choose the upgrades
@ -34,37 +50,202 @@ import org.springframework.boot.build.bom.Library;
*/ */
public final class InteractiveUpgradeResolver implements UpgradeResolver { public final class InteractiveUpgradeResolver implements UpgradeResolver {
private final UserInputHandler userInputHandler; private final VersionResolver versionResolver;
private final UpgradePolicy upgradePolicy;
private final LibraryUpdateResolver libraryUpdateResolver; private final UserInputHandler userInputHandler;
InteractiveUpgradeResolver(UserInputHandler userInputHandler, LibraryUpdateResolver libraryUpdateResolver) { InteractiveUpgradeResolver(VersionResolver versionResolver, UpgradePolicy upgradePolicy,
UserInputHandler userInputHandler) {
this.versionResolver = versionResolver;
this.upgradePolicy = upgradePolicy;
this.userInputHandler = userInputHandler; this.userInputHandler = userInputHandler;
this.libraryUpdateResolver = libraryUpdateResolver;
} }
@Override @Override
public List<Upgrade> resolveUpgrades(Collection<Library> librariesToUpgrade, Collection<Library> libraries) { public List<Upgrade> resolveUpgrades(Collection<Library> libraries) {
Map<String, Library> librariesByName = new HashMap<>(); Map<String, Library> librariesByName = new HashMap<>();
for (Library library : libraries) { for (Library library : libraries) {
librariesByName.put(library.getName(), library); librariesByName.put(library.getName(), library);
} }
List<LibraryWithVersionOptions> libraryUpdates = this.libraryUpdateResolver return libraries.stream().filter((library) -> !library.getName().equals("Spring Boot"))
.findLibraryUpdates(librariesToUpgrade, librariesByName); .map((library) -> resolveUpgrade(library, librariesByName)).filter((upgrade) -> upgrade != null)
return libraryUpdates.stream().map(this::resolveUpgrade).filter(Objects::nonNull).toList(); .collect(Collectors.toList());
} }
private Upgrade resolveUpgrade(LibraryWithVersionOptions libraryWithVersionOptions) { private Upgrade resolveUpgrade(Library library, Map<String, Library> libraries) {
if (libraryWithVersionOptions.getVersionOptions().isEmpty()) { List<VersionOption> versionOptions = getVersionOptions(library, libraries);
if (versionOptions.isEmpty()) {
return null; return null;
} }
VersionOption current = new VersionOption(libraryWithVersionOptions.getLibrary().getVersion().getVersion()); VersionOption current = new VersionOption(library.getVersion().getVersion());
VersionOption selected = this.userInputHandler.selectOption( VersionOption selected = this.userInputHandler
libraryWithVersionOptions.getLibrary().getName() + " " .selectOption(library.getName() + " " + library.getVersion().getVersion(), versionOptions, current);
+ libraryWithVersionOptions.getLibrary().getVersion().getVersion(), return (selected.equals(current)) ? null : new Upgrade(library, selected.version);
libraryWithVersionOptions.getVersionOptions(), current); }
return (selected.equals(current)) ? null
: new Upgrade(libraryWithVersionOptions.getLibrary(), selected.getVersion()); private List<VersionOption> getVersionOptions(Library library, Map<String, Library> libraries) {
if (library.getVersion().getVersionAlignment() != null) {
return determineAlignedVersionOption(library, libraries);
}
return determineResolvedVersionOptions(library);
}
private List<VersionOption> determineResolvedVersionOptions(Library library) {
Map<String, SortedSet<DependencyVersion>> moduleVersions = new LinkedHashMap<>();
DependencyVersion libraryVersion = library.getVersion().getVersion();
for (Group group : library.getGroups()) {
for (Module module : group.getModules()) {
moduleVersions.put(group.getId() + ":" + module.getName(),
getLaterVersionsForModule(group.getId(), module.getName(), libraryVersion));
}
for (String bom : group.getBoms()) {
moduleVersions.put(group.getId() + ":" + bom,
getLaterVersionsForModule(group.getId(), bom, libraryVersion));
}
for (String plugin : group.getPlugins()) {
moduleVersions.put(group.getId() + ":" + plugin,
getLaterVersionsForModule(group.getId(), plugin, libraryVersion));
}
}
List<DependencyVersion> allVersions = moduleVersions.values().stream().flatMap(SortedSet::stream).distinct()
.filter((dependencyVersion) -> isPermitted(dependencyVersion, library.getProhibitedVersions()))
.collect(Collectors.toList());
if (allVersions.isEmpty()) {
return Collections.emptyList();
}
return allVersions.stream()
.map((version) -> new ResolvedVersionOption(version, getMissingModules(moduleVersions, version)))
.collect(Collectors.toList());
}
private List<VersionOption> determineAlignedVersionOption(Library library, Map<String, Library> libraries) {
VersionOption alignedVersionOption = alignedVersionOption(library, libraries);
if (alignedVersionOption == null) {
return Collections.emptyList();
}
if (!isPermitted(alignedVersionOption.version, library.getProhibitedVersions())) {
throw new InvalidUserDataException("Version alignment failed. Version " + alignedVersionOption.version
+ " from " + library.getName() + " is prohibited");
}
return Collections.singletonList(alignedVersionOption);
}
private VersionOption alignedVersionOption(Library library, Map<String, Library> libraries) {
VersionAlignment versionAlignment = library.getVersion().getVersionAlignment();
Library alignmentLibrary = libraries.get(versionAlignment.getLibraryName());
DependencyVersions dependencyVersions = alignmentLibrary.getDependencyVersions();
if (dependencyVersions == null) {
throw new InvalidUserDataException("Cannot align with library '" + versionAlignment.getLibraryName()
+ "' as it does not define any dependency versions");
}
if (!dependencyVersions.available()) {
return null;
}
Set<String> versions = new HashSet<>();
for (Group group : library.getGroups()) {
for (Module module : group.getModules()) {
String version = dependencyVersions.getVersion(group.getId(), module.getName());
if (version != null) {
versions.add(version);
}
}
}
if (versions.isEmpty()) {
throw new InvalidUserDataException("Cannot align with library '" + versionAlignment.getLibraryName()
+ "' as its dependency versions do not include any of this library's modules");
}
if (versions.size() > 1) {
throw new InvalidUserDataException("Cannot align with library '" + versionAlignment.getLibraryName()
+ "' as it uses multiple different versions of this library's modules");
}
DependencyVersion version = DependencyVersion.parse(versions.iterator().next());
return library.getVersion().getVersion().equals(version) ? null
: new AlignedVersionOption(version, alignmentLibrary);
}
private boolean isPermitted(DependencyVersion dependencyVersion, List<ProhibitedVersion> prohibitedVersions) {
if (prohibitedVersions.isEmpty()) {
return true;
}
for (ProhibitedVersion prohibitedVersion : prohibitedVersions) {
if (prohibitedVersion.getRange()
.containsVersion(new DefaultArtifactVersion(dependencyVersion.toString()))) {
return false;
}
}
return true;
}
private List<String> getMissingModules(Map<String, SortedSet<DependencyVersion>> moduleVersions,
DependencyVersion version) {
List<String> missingModules = new ArrayList<>();
moduleVersions.forEach((name, versions) -> {
if (!versions.contains(version)) {
missingModules.add(name);
}
});
return missingModules;
}
private SortedSet<DependencyVersion> getLaterVersionsForModule(String groupId, String artifactId,
DependencyVersion currentVersion) {
SortedSet<DependencyVersion> versions = this.versionResolver.resolveVersions(groupId, artifactId);
versions.removeIf((candidate) -> !this.upgradePolicy.test(candidate, currentVersion));
return versions;
}
private static class VersionOption {
private final DependencyVersion version;
protected VersionOption(DependencyVersion version) {
this.version = version;
}
@Override
public String toString() {
return this.version.toString();
}
}
private static final class AlignedVersionOption extends VersionOption {
private final Library alignedWith;
private AlignedVersionOption(DependencyVersion version, Library alignedWith) {
super(version);
this.alignedWith = alignedWith;
}
@Override
public String toString() {
return super.toString() + " (aligned with " + this.alignedWith.getName() + " "
+ this.alignedWith.getVersion().getVersion() + ")";
}
}
private static final class ResolvedVersionOption extends VersionOption {
private final List<String> missingModules;
private ResolvedVersionOption(DependencyVersion version, List<String> missingModules) {
super(version);
this.missingModules = missingModules;
}
@Override
public String toString() {
if (this.missingModules.isEmpty()) {
return super.toString();
}
return super.toString() + " (some modules are missing: "
+ StringUtils.collectionToDelimitedString(this.missingModules, ", ") + ")";
}
} }
} }

@ -1,41 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.bom.bomr;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.boot.build.bom.Library;
/**
* Resolves library updates.
*
* @author Moritz Halbritter
*/
public interface LibraryUpdateResolver {
/**
* Finds library updates.
* @param librariesToUpgrade libraries to update
* @param librariesByName libraries indexed by name
* @return library which have updates
*/
List<LibraryWithVersionOptions> findLibraryUpdates(Collection<Library> librariesToUpgrade,
Map<String, Library> librariesByName);
}

@ -1,42 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.bom.bomr;
import java.util.List;
import org.springframework.boot.build.bom.Library;
class LibraryWithVersionOptions {
private final Library library;
private final List<VersionOption> versionOptions;
LibraryWithVersionOptions(Library library, List<VersionOption> versionOptions) {
this.library = library;
this.versionOptions = versionOptions;
}
Library getLibrary() {
return this.library;
}
List<VersionOption> getVersionOptions() {
return this.versionOptions;
}
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,10 +17,9 @@
package org.springframework.boot.build.bom.bomr; package org.springframework.boot.build.bom.bomr;
import java.io.StringReader; import java.io.StringReader;
import java.net.URI; import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.SortedSet; import java.util.SortedSet;
import java.util.TreeSet; import java.util.TreeSet;
@ -50,48 +49,35 @@ final class MavenMetadataVersionResolver implements VersionResolver {
private final RestTemplate rest; private final RestTemplate rest;
private final Collection<URI> repositoryUrls; private final List<String> repositoryUrls;
MavenMetadataVersionResolver(Collection<URI> repositoryUrls) { MavenMetadataVersionResolver(List<String> repositoryUrls) {
this(new RestTemplate(Collections.singletonList(new StringHttpMessageConverter())), repositoryUrls); this(new RestTemplate(Arrays.asList(new StringHttpMessageConverter())), repositoryUrls);
} }
MavenMetadataVersionResolver(RestTemplate restTemplate, Collection<URI> repositoryUrls) { MavenMetadataVersionResolver(RestTemplate restTemplate, List<String> repositoryUrls) {
this.rest = restTemplate; this.rest = restTemplate;
this.repositoryUrls = normalize(repositoryUrls); this.repositoryUrls = repositoryUrls;
}
private Collection<URI> normalize(Collection<URI> uris) {
return uris.stream().map(this::normalize).toList();
}
private URI normalize(URI uri) {
if ("/".equals(uri.getPath())) {
return uri;
}
return URI.create(uri.toString() + "/");
} }
@Override @Override
public SortedSet<DependencyVersion> resolveVersions(String groupId, String artifactId) { public SortedSet<DependencyVersion> resolveVersions(String groupId, String artifactId) {
Set<String> versions = new HashSet<>(); Set<String> versions = new HashSet<>();
for (URI repositoryUrl : this.repositoryUrls) { for (String repositoryUrl : this.repositoryUrls) {
versions.addAll(resolveVersions(groupId, artifactId, repositoryUrl)); versions.addAll(resolveVersions(groupId, artifactId, repositoryUrl));
} }
return versions.stream().map(DependencyVersion::parse).collect(Collectors.toCollection(TreeSet::new)); return new TreeSet<>(versions.stream().map(DependencyVersion::parse).collect(Collectors.toSet()));
} }
private Set<String> resolveVersions(String groupId, String artifactId, URI repositoryUrl) { private Set<String> resolveVersions(String groupId, String artifactId, String repositoryUrl) {
Set<String> versions = new HashSet<>(); Set<String> versions = new HashSet<>();
URI url = repositoryUrl.resolve(groupId.replace('.', '/') + "/" + artifactId + "/maven-metadata.xml"); String url = repositoryUrl + "/" + groupId.replace('.', '/') + "/" + artifactId + "/maven-metadata.xml";
try { try {
String metadata = this.rest.getForObject(url, String.class); String metadata = this.rest.getForObject(url, String.class);
Document metadataDocument = DocumentBuilderFactory.newInstance() Document metadataDocument = DocumentBuilderFactory.newInstance().newDocumentBuilder()
.newDocumentBuilder() .parse(new InputSource(new StringReader(metadata)));
.parse(new InputSource(new StringReader(metadata))); NodeList versionNodes = (NodeList) XPathFactory.newInstance().newXPath()
NodeList versionNodes = (NodeList) XPathFactory.newInstance() .evaluate("/metadata/versioning/versions/version", metadataDocument, XPathConstants.NODESET);
.newXPath()
.evaluate("/metadata/versioning/versions/version", metadataDocument, XPathConstants.NODESET);
for (int i = 0; i < versionNodes.getLength(); i++) { for (int i = 0; i < versionNodes.getLength(); i++) {
versions.add(versionNodes.item(i).getTextContent()); versions.add(versionNodes.item(i).getTextContent());
} }

@ -1,108 +0,0 @@
/*
* Copyright 2021-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.bom.bomr;
import java.net.URI;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.function.BiPredicate;
import javax.inject.Inject;
import org.gradle.api.Task;
import org.gradle.api.tasks.TaskAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.build.bom.BomExtension;
import org.springframework.boot.build.bom.Library;
import org.springframework.boot.build.bom.bomr.ReleaseSchedule.Release;
import org.springframework.boot.build.bom.bomr.github.Milestone;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
/**
* A {@link Task} to move to snapshot dependencies.
*
* @author Andy Wilkinson
*/
public abstract class MoveToSnapshots extends UpgradeDependencies {
private static final Logger log = LoggerFactory.getLogger(MoveToSnapshots.class);
private final URI REPOSITORY_URI = URI.create("https://repo.spring.io/snapshot/");
@Inject
public MoveToSnapshots(BomExtension bom) {
super(bom, true);
getRepositoryUris().add(this.REPOSITORY_URI);
}
@Override
@TaskAction
void upgradeDependencies() {
super.upgradeDependencies();
}
@Override
protected String issueTitle(Upgrade upgrade) {
String snapshotVersion = upgrade.getVersion().toString();
String releaseVersion = snapshotVersion.substring(0, snapshotVersion.length() - "-SNAPSHOT".length());
return "Upgrade to " + upgrade.getLibrary().getName() + " " + releaseVersion;
}
@Override
protected String commitMessage(Upgrade upgrade, int issueNumber) {
return "Start building against " + upgrade.getLibrary().getName() + " " + releaseVersion(upgrade) + " snapshots"
+ "\n\nSee gh-" + issueNumber;
}
private String releaseVersion(Upgrade upgrade) {
String snapshotVersion = upgrade.getVersion().toString();
return snapshotVersion.substring(0, snapshotVersion.length() - "-SNAPSHOT".length());
}
@Override
protected boolean eligible(Library library) {
return library.isConsiderSnapshots() && super.eligible(library);
}
@Override
protected List<BiPredicate<Library, DependencyVersion>> determineUpdatePredicates(Milestone milestone) {
ReleaseSchedule releaseSchedule = new ReleaseSchedule();
Map<String, List<Release>> releases = releaseSchedule.releasesBetween(OffsetDateTime.now(),
milestone.getDueOn());
List<BiPredicate<Library, DependencyVersion>> predicates = super.determineUpdatePredicates(milestone);
predicates.add((library, candidate) -> {
List<Release> releasesForLibrary = releases.get(library.getCalendarName());
if (releasesForLibrary != null) {
for (Release release : releasesForLibrary) {
if (candidate.isSnapshotFor(release.getVersion())) {
return true;
}
}
}
if (log.isInfoEnabled()) {
log.info("Ignoring " + candidate + ". No release of " + library.getName() + " scheduled before "
+ milestone.getDueOn());
}
return false;
});
return predicates;
}
}

@ -1,84 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.bom.bomr;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.build.bom.Library;
/**
* {@link LibraryUpdateResolver} decorator that uses multiple threads to find library
* updates.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
*/
class MultithreadedLibraryUpdateResolver implements LibraryUpdateResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(MultithreadedLibraryUpdateResolver.class);
private final int threads;
private final LibraryUpdateResolver delegate;
MultithreadedLibraryUpdateResolver(int threads, LibraryUpdateResolver delegate) {
this.threads = threads;
this.delegate = delegate;
}
@Override
public List<LibraryWithVersionOptions> findLibraryUpdates(Collection<Library> librariesToUpgrade,
Map<String, Library> librariesByName) {
LOGGER.info("Looking for updates using {} threads", this.threads);
ExecutorService executorService = Executors.newFixedThreadPool(this.threads);
try {
return librariesToUpgrade.stream()
.map((library) -> executorService.submit(
() -> this.delegate.findLibraryUpdates(Collections.singletonList(library), librariesByName)))
.flatMap(this::getResult)
.toList();
}
finally {
executorService.shutdownNow();
}
}
private Stream<LibraryWithVersionOptions> getResult(Future<List<LibraryWithVersionOptions>> job) {
try {
return job.get().stream();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
catch (ExecutionException ex) {
throw new RuntimeException(ex);
}
}
}

@ -1,108 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.bom.bomr;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
/**
* Release schedule for Spring projects, retrieved from
* <a href="https://calendar.spring.io">https://calendar.spring.io</a>.
*
* @author Andy Wilkinson
*/
class ReleaseSchedule {
private static final Pattern LIBRARY_AND_VERSION = Pattern.compile("([A-Za-z0-9 ]+) ([0-9A-Za-z.-]+)");
private final RestOperations rest;
ReleaseSchedule() {
this(new RestTemplate());
}
ReleaseSchedule(RestOperations rest) {
this.rest = rest;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
Map<String, List<Release>> releasesBetween(OffsetDateTime start, OffsetDateTime end) {
ResponseEntity<List> response = this.rest
.getForEntity("https://calendar.spring.io/releases?start=" + start + "&end=" + end, List.class);
List<Map<String, String>> body = response.getBody();
Map<String, List<Release>> releasesByLibrary = new LinkedCaseInsensitiveMap<>();
body.stream()
.map(this::asRelease)
.filter(Objects::nonNull)
.forEach((release) -> releasesByLibrary.computeIfAbsent(release.getLibraryName(), (l) -> new ArrayList<>())
.add(release));
return releasesByLibrary;
}
private Release asRelease(Map<String, String> entry) {
LocalDate due = LocalDate.parse(entry.get("start"));
String title = entry.get("title");
Matcher matcher = LIBRARY_AND_VERSION.matcher(title);
if (!matcher.matches()) {
return null;
}
String library = matcher.group(1);
String version = matcher.group(2);
return new Release(library, DependencyVersion.parse(version), due);
}
static class Release {
private final String libraryName;
private final DependencyVersion version;
private final LocalDate dueOn;
Release(String libraryName, DependencyVersion version, LocalDate dueOn) {
this.libraryName = libraryName;
this.version = version;
this.dueOn = dueOn;
}
String getLibraryName() {
return this.libraryName;
}
DependencyVersion getVersion() {
return this.version;
}
LocalDate getDueOn() {
return this.dueOn;
}
}
}

@ -1,122 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.bom.bomr;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.function.BiPredicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.bomr.version.DependencyVersion;
/**
* Standard implementation for {@link LibraryUpdateResolver}.
*
* @author Andy Wilkinson
*/
class StandardLibraryUpdateResolver implements LibraryUpdateResolver {
private static final Logger LOGGER = LoggerFactory.getLogger(StandardLibraryUpdateResolver.class);
private final VersionResolver versionResolver;
private final BiPredicate<Library, DependencyVersion> predicate;
StandardLibraryUpdateResolver(VersionResolver versionResolver,
List<BiPredicate<Library, DependencyVersion>> predicates) {
this.versionResolver = versionResolver;
this.predicate = (library, dependencyVersion) -> predicates.stream()
.allMatch((predicate) -> predicate.test(library, dependencyVersion));
}
@Override
public List<LibraryWithVersionOptions> findLibraryUpdates(Collection<Library> librariesToUpgrade,
Map<String, Library> librariesByName) {
List<LibraryWithVersionOptions> result = new ArrayList<>();
for (Library library : librariesToUpgrade) {
if (isLibraryExcluded(library)) {
continue;
}
LOGGER.info("Looking for updates for {}", library.getName());
long start = System.nanoTime();
List<VersionOption> versionOptions = getVersionOptions(library, librariesByName);
result.add(new LibraryWithVersionOptions(library, versionOptions));
LOGGER.info("Found {} updates for {}, took {}", versionOptions.size(), library.getName(),
Duration.ofNanos(System.nanoTime() - start));
}
return result;
}
protected boolean isLibraryExcluded(Library library) {
return library.getName().equals("Spring Boot");
}
protected List<VersionOption> getVersionOptions(Library library, Map<String, Library> libraries) {
return determineResolvedVersionOptions(library);
}
private List<VersionOption> determineResolvedVersionOptions(Library library) {
Map<String, SortedSet<DependencyVersion>> 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));
}
for (String bom : group.getBoms()) {
moduleVersions.put(group.getId() + ":" + bom, getLaterVersionsForModule(group.getId(), bom, library));
}
for (String plugin : group.getPlugins()) {
moduleVersions.put(group.getId() + ":" + plugin,
getLaterVersionsForModule(group.getId(), plugin, library));
}
}
return moduleVersions.values()
.stream()
.flatMap(SortedSet::stream)
.distinct()
.filter((dependencyVersion) -> this.predicate.test(library, dependencyVersion))
.map((version) -> (VersionOption) new VersionOption.ResolvedVersionOption(version,
getMissingModules(moduleVersions, version)))
.toList();
}
private List<String> getMissingModules(Map<String, SortedSet<DependencyVersion>> moduleVersions,
DependencyVersion version) {
List<String> missingModules = new ArrayList<>();
moduleVersions.forEach((name, versions) -> {
if (!versions.contains(version)) {
missingModules.add(name);
}
});
return missingModules;
}
private SortedSet<DependencyVersion> getLaterVersionsForModule(String groupId, String artifactId, Library library) {
return this.versionResolver.resolveVersions(groupId, artifactId);
}
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2022 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,7 +20,7 @@ import org.springframework.boot.build.bom.Library;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion; import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
/** /**
* An upgrade to change a {@link Library} to use a new version. * An upgrade to change a {@link Library} to use a new version}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +17,7 @@
package org.springframework.boot.build.bom.bomr; package org.springframework.boot.build.bom.bomr;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
@ -41,14 +42,14 @@ class UpgradeApplicator {
} }
Path apply(Upgrade upgrade) throws IOException { Path apply(Upgrade upgrade) throws IOException {
String buildFileContents = Files.readString(this.buildFile); String buildFileContents = new String(Files.readAllBytes(this.buildFile), StandardCharsets.UTF_8);
Matcher matcher = Pattern.compile("library\\(\"" + upgrade.getLibrary().getName() + "\", \"(.+)\"\\)") Matcher matcher = Pattern.compile("library\\(\"" + upgrade.getLibrary().getName() + "\", \"(.+)\"\\)")
.matcher(buildFileContents); .matcher(buildFileContents);
if (!matcher.find()) { if (!matcher.find()) {
matcher = Pattern matcher = Pattern
.compile("library\\(\"" + upgrade.getLibrary().getName() + "\"\\) \\{\\s+version\\(\"(.+)\"\\)", .compile("library\\(\"" + upgrade.getLibrary().getName() + "\"\\) \\{\\s+version\\(\"(.+)\"\\)",
Pattern.MULTILINE) Pattern.MULTILINE)
.matcher(buildFileContents); .matcher(buildFileContents);
if (!matcher.find()) { if (!matcher.find()) {
throw new IllegalStateException("Failed to find definition for library '" throw new IllegalStateException("Failed to find definition for library '"
+ upgrade.getLibrary().getName() + "' in bom '" + this.buildFile + "'"); + upgrade.getLibrary().getName() + "' in bom '" + this.buildFile + "'");
@ -67,7 +68,7 @@ class UpgradeApplicator {
private void updateGradleProperties(Upgrade upgrade, String version) throws IOException { private void updateGradleProperties(Upgrade upgrade, String version) throws IOException {
String property = version.substring(2, version.length() - 1); String property = version.substring(2, version.length() - 1);
String gradlePropertiesContents = Files.readString(this.gradleProperties); String gradlePropertiesContents = new String(Files.readAllBytes(this.gradleProperties), StandardCharsets.UTF_8);
String modified = gradlePropertiesContents.replace( String modified = gradlePropertiesContents.replace(
property + "=" + upgrade.getLibrary().getVersion().getVersion(), property + "=" + upgrade.getVersion()); property + "=" + upgrade.getLibrary().getVersion().getVersion(), property + "=" + upgrade.getVersion());
overwrite(this.gradleProperties, modified); overwrite(this.gradleProperties, modified);
@ -81,7 +82,8 @@ class UpgradeApplicator {
} }
private void overwrite(Path target, String content) throws IOException { private void overwrite(Path target, String content) throws IOException {
Files.writeString(target, content, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); Files.write(target, content.getBytes(StandardCharsets.UTF_8), StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING);
} }
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,42 +16,153 @@
package org.springframework.boot.build.bom.bomr; package org.springframework.boot.build.bom.bomr;
import java.net.URI; import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Path;
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 javax.inject.Inject;
import org.gradle.api.DefaultTask;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.Task; import org.gradle.api.Task;
import org.gradle.api.artifacts.repositories.MavenArtifactRepository; import org.gradle.api.internal.tasks.userinput.UserInputHandler;
import org.gradle.api.tasks.Input;
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.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.Issue;
import org.springframework.boot.build.bom.bomr.github.Milestone;
import org.springframework.util.StringUtils;
/** /**
* {@link Task} to upgrade the libraries managed by a bom. * {@link Task} to upgrade the libraries managed by a bom.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Moritz Halbritter
*/ */
public abstract class UpgradeBom extends UpgradeDependencies { public class UpgradeBom extends DefaultTask {
private final BomExtension bom;
private String milestone;
@Inject @Inject
public UpgradeBom(BomExtension bom) { public UpgradeBom(BomExtension bom) {
super(bom); this.bom = bom;
getProject().getRepositories().withType(MavenArtifactRepository.class, (repository) -> { }
URI repositoryUrl = repository.getUrl();
if (!repositoryUrl.toString().endsWith("snapshot")) { @Option(option = "milestone", description = "Milestone to which dependency upgrade issues should be assigned")
getRepositoryUris().add(repositoryUrl); public void setMilestone(String milestone) {
this.milestone = milestone;
}
@Input
public String getMilestone() {
return this.milestone;
}
@TaskAction
void upgradeDependencies() {
GitHubRepository repository = createGitHub().getRepository(this.bom.getUpgrade().getGitHub().getOrganization(),
this.bom.getUpgrade().getGitHub().getRepository());
List<String> availableLabels = repository.getLabels();
List<String> issueLabels = this.bom.getUpgrade().getGitHub().getIssueLabels();
if (!availableLabels.containsAll(issueLabels)) {
List<String> unknownLabels = new ArrayList<>(issueLabels);
unknownLabels.removeAll(availableLabels);
throw new InvalidUserDataException(
"Unknown label(s): " + StringUtils.collectionToCommaDelimitedString(unknownLabels));
}
Milestone milestone = determineMilestone(repository);
List<Issue> existingUpgradeIssues = repository.findIssues(issueLabels, milestone);
List<Upgrade> upgrades = new InteractiveUpgradeResolver(
new MavenMetadataVersionResolver(Arrays.asList("https://repo1.maven.org/maven2/")),
this.bom.getUpgrade().getPolicy(), getServices().get(UserInputHandler.class))
.resolveUpgrades(this.bom.getLibraries());
Path buildFile = getProject().getBuildFile().toPath();
Path gradleProperties = new File(getProject().getRootProject().getProjectDir(), "gradle.properties").toPath();
UpgradeApplicator upgradeApplicator = new UpgradeApplicator(buildFile, gradleProperties);
for (Upgrade upgrade : upgrades) {
String title = "Upgrade to " + upgrade.getLibrary().getName() + " " + upgrade.getVersion();
Issue existingUpgradeIssue = findExistingUpgradeIssue(existingUpgradeIssues, upgrade);
if (existingUpgradeIssue != null) {
System.out.println(title + " (supersedes #" + existingUpgradeIssue.getNumber() + " "
+ existingUpgradeIssue.getTitle() + ")");
}
else {
System.out.println(title);
}
try {
Path modified = upgradeApplicator.apply(upgrade);
int issueNumber = repository.openIssue(title,
(existingUpgradeIssue != null) ? "Supersedes #" + existingUpgradeIssue.getNumber() : "",
issueLabels, milestone);
if (existingUpgradeIssue != null) {
existingUpgradeIssue.label(Arrays.asList("type: task", "status: superseded"));
}
if (new ProcessBuilder().command("git", "add", modified.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 Issue findExistingUpgradeIssue(List<Issue> existingUpgradeIssues, Upgrade upgrade) {
String toMatch = "Upgrade to " + upgrade.getLibrary().getName();
for (Issue existingUpgradeIssue : existingUpgradeIssues) {
if (existingUpgradeIssue.getTitle().substring(0, existingUpgradeIssue.getTitle().lastIndexOf(' '))
.equals(toMatch)) {
return existingUpgradeIssue;
} }
}); }
return null;
} }
@Override private GitHub createGitHub() {
protected String issueTitle(Upgrade upgrade) { Properties bomrProperties = new Properties();
return "Upgrade to " + upgrade.getLibrary().getName() + " " + upgrade.getVersion(); 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);
}
} }
@Override private Milestone determineMilestone(GitHubRepository repository) {
protected String commitMessage(Upgrade upgrade, int issueNumber) { if (this.milestone == null) {
return issueTitle(upgrade) + "\n\nCloses gh-" + issueNumber; return null;
}
List<Milestone> milestones = repository.getMilestones();
Optional<Milestone> matchingMilestone = milestones.stream()
.filter((milestone) -> milestone.getName().equals(this.milestone)).findFirst();
if (!matchingMilestone.isPresent()) {
throw new InvalidUserDataException("Unknown milestone: " + this.milestone);
}
return matchingMilestone.get();
} }
} }

@ -1,283 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.bom.bomr;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import javax.inject.Inject;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.VersionRange;
import org.gradle.api.DefaultTask;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.internal.tasks.userinput.UserInputHandler;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.Optional;
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.Library;
import org.springframework.boot.build.bom.Library.ProhibitedVersion;
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.Issue;
import org.springframework.boot.build.bom.bomr.github.Milestone;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
import org.springframework.util.StringUtils;
/**
* Base class for tasks that upgrade dependencies in a bom.
*
* @author Andy Wilkinson
* @author Moritz Halbritter
*/
public abstract class UpgradeDependencies extends DefaultTask {
private final BomExtension bom;
private final boolean movingToSnapshots;
@Inject
public UpgradeDependencies(BomExtension bom) {
this(bom, false);
}
protected UpgradeDependencies(BomExtension bom, boolean movingToSnapshots) {
this.bom = bom;
getThreads().convention(2);
this.movingToSnapshots = movingToSnapshots;
}
@Input
@Option(option = "milestone", description = "Milestone to which dependency upgrade issues should be assigned")
public abstract Property<String> getMilestone();
@Input
@Optional
@Option(option = "threads", description = "Number of Threads to use for update resolution")
public abstract Property<Integer> getThreads();
@Input
@Optional
@Option(option = "libraries", description = "Regular expression that identifies the libraries to upgrade")
public abstract Property<String> getLibraries();
@Input
abstract ListProperty<URI> getRepositoryUris();
@TaskAction
void upgradeDependencies() {
GitHubRepository repository = createGitHub().getRepository(this.bom.getUpgrade().getGitHub().getOrganization(),
this.bom.getUpgrade().getGitHub().getRepository());
List<String> issueLabels = verifyLabels(repository);
Milestone milestone = determineMilestone(repository);
List<Upgrade> upgrades = resolveUpgrades(milestone);
applyUpgrades(repository, issueLabels, milestone, upgrades);
}
private void applyUpgrades(GitHubRepository repository, List<String> issueLabels, Milestone milestone,
List<Upgrade> upgrades) {
Path buildFile = getProject().getBuildFile().toPath();
Path gradleProperties = new File(getProject().getRootProject().getProjectDir(), "gradle.properties").toPath();
UpgradeApplicator upgradeApplicator = new UpgradeApplicator(buildFile, gradleProperties);
List<Issue> existingUpgradeIssues = repository.findIssues(issueLabels, milestone);
System.out.println("Applying upgrades...");
System.out.println("");
for (Upgrade upgrade : upgrades) {
System.out.println(upgrade.getLibrary().getName() + " " + upgrade.getVersion());
String title = issueTitle(upgrade);
Issue existingUpgradeIssue = findExistingUpgradeIssue(existingUpgradeIssues, upgrade);
try {
Path modified = upgradeApplicator.apply(upgrade);
int issueNumber = getOrOpenUpgradeIssue(repository, issueLabels, milestone, title,
existingUpgradeIssue);
if (existingUpgradeIssue != null && existingUpgradeIssue.getState() == Issue.State.CLOSED) {
existingUpgradeIssue.label(Arrays.asList("type: task", "status: superseded"));
}
System.out.println(" Issue: " + issueNumber + " - " + title
+ getExistingUpgradeIssueMessageDetails(existingUpgradeIssue));
if (new ProcessBuilder().command("git", "add", modified.toFile().getAbsolutePath())
.start()
.waitFor() != 0) {
throw new IllegalStateException("git add failed");
}
String commitMessage = commitMessage(upgrade, issueNumber);
if (new ProcessBuilder().command("git", "commit", "-m", commitMessage).start().waitFor() != 0) {
throw new IllegalStateException("git commit failed");
}
System.out.println(" Commit: " + commitMessage.substring(0, commitMessage.indexOf('\n')));
}
catch (IOException ex) {
throw new TaskExecutionException(this, ex);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
private int getOrOpenUpgradeIssue(GitHubRepository repository, List<String> issueLabels, Milestone milestone,
String title, Issue existingUpgradeIssue) {
if (existingUpgradeIssue != null && existingUpgradeIssue.getState() == Issue.State.OPEN) {
return existingUpgradeIssue.getNumber();
}
String body = (existingUpgradeIssue != null) ? "Supersedes #" + existingUpgradeIssue.getNumber() : "";
return repository.openIssue(title, body, issueLabels, milestone);
}
private String getExistingUpgradeIssueMessageDetails(Issue existingUpgradeIssue) {
if (existingUpgradeIssue == null) {
return "";
}
if (existingUpgradeIssue.getState() != Issue.State.CLOSED) {
return " (completes existing upgrade)";
}
return " (supersedes #" + existingUpgradeIssue.getNumber() + " " + existingUpgradeIssue.getTitle() + ")";
}
private List<String> verifyLabels(GitHubRepository repository) {
Set<String> availableLabels = repository.getLabels();
List<String> issueLabels = this.bom.getUpgrade().getGitHub().getIssueLabels();
if (!availableLabels.containsAll(issueLabels)) {
List<String> unknownLabels = new ArrayList<>(issueLabels);
unknownLabels.removeAll(availableLabels);
throw new InvalidUserDataException(
"Unknown label(s): " + StringUtils.collectionToCommaDelimitedString(unknownLabels));
}
return issueLabels;
}
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 Milestone determineMilestone(GitHubRepository repository) {
List<Milestone> milestones = repository.getMilestones();
java.util.Optional<Milestone> matchingMilestone = milestones.stream()
.filter((milestone) -> milestone.getName().equals(getMilestone().get()))
.findFirst();
if (!matchingMilestone.isPresent()) {
throw new InvalidUserDataException("Unknown milestone: " + getMilestone().get());
}
return matchingMilestone.get();
}
private Issue findExistingUpgradeIssue(List<Issue> existingUpgradeIssues, Upgrade upgrade) {
String toMatch = "Upgrade to " + upgrade.getLibrary().getName();
for (Issue existingUpgradeIssue : existingUpgradeIssues) {
String title = existingUpgradeIssue.getTitle();
int lastSpaceIndex = title.lastIndexOf(' ');
if (lastSpaceIndex > -1) {
title = title.substring(0, lastSpaceIndex);
}
if (title.equals(toMatch)) {
return existingUpgradeIssue;
}
}
return null;
}
@SuppressWarnings("deprecation")
private List<Upgrade> resolveUpgrades(Milestone milestone) {
List<Upgrade> upgrades = new InteractiveUpgradeResolver(getServices().get(UserInputHandler.class),
new MultithreadedLibraryUpdateResolver(getThreads().get(),
new StandardLibraryUpdateResolver(new MavenMetadataVersionResolver(getRepositoryUris().get()),
determineUpdatePredicates(milestone))))
.resolveUpgrades(matchingLibraries(), this.bom.getLibraries());
return upgrades;
}
protected List<BiPredicate<Library, DependencyVersion>> determineUpdatePredicates(Milestone milestone) {
List<BiPredicate<Library, DependencyVersion>> updatePredicates = new ArrayList<>();
updatePredicates.add(this::compilesWithUpgradePolicy);
updatePredicates.add(this::isAnUpgrade);
updatePredicates.add(this::isNotProhibited);
return updatePredicates;
}
private boolean compilesWithUpgradePolicy(Library library, DependencyVersion candidate) {
return this.bom.getUpgrade().getPolicy().test(candidate, library.getVersion().getVersion());
}
private boolean isAnUpgrade(Library library, DependencyVersion candidate) {
return library.getVersion().getVersion().isUpgrade(candidate, this.movingToSnapshots);
}
private boolean isNotProhibited(Library library, DependencyVersion candidate) {
return !library.getProhibitedVersions()
.stream()
.anyMatch((prohibited) -> isProhibited(prohibited, candidate.toString()));
}
private boolean isProhibited(ProhibitedVersion prohibited, String candidate) {
boolean result = false;
VersionRange range = prohibited.getRange();
result = result || (range != null && range.containsVersion(new DefaultArtifactVersion(candidate)));
result = result || prohibited.getStartsWith().stream().anyMatch(candidate::startsWith);
result = result || prohibited.getStartsWith().stream().anyMatch(candidate::endsWith);
result = result || prohibited.getStartsWith().stream().anyMatch(candidate::contains);
return result;
}
private List<Library> matchingLibraries() {
List<Library> matchingLibraries = this.bom.getLibraries().stream().filter(this::eligible).toList();
if (matchingLibraries.isEmpty()) {
throw new InvalidUserDataException("No libraries to upgrade");
}
return matchingLibraries;
}
protected boolean eligible(Library library) {
String pattern = getLibraries().getOrNull();
if (pattern == null) {
return true;
}
Predicate<String> libraryPredicate = Pattern.compile(pattern).asPredicate();
return libraryPredicate.test(library.getName());
}
protected abstract String issueTitle(Upgrade upgrade);
protected abstract String commitMessage(Upgrade upgrade, int issueNumber);
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2022 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,10 +30,9 @@ interface UpgradeResolver {
/** /**
* Resolves the upgrades to be applied to the given {@code libraries}. * Resolves the upgrades to be applied to the given {@code libraries}.
* @param librariesToUpgrade the libraries to upgrade * @param libraries the libraries
* @param libraries all libraries
* @return the upgrades * @return the upgrades
*/ */
List<Upgrade> resolveUpgrades(Collection<Library> librariesToUpgrade, Collection<Library> libraries); List<Upgrade> resolveUpgrades(Collection<Library> libraries);
} }

@ -1,84 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.bom.bomr;
import java.util.List;
import org.springframework.boot.build.bom.Library;
import org.springframework.boot.build.bom.bomr.version.DependencyVersion;
import org.springframework.util.StringUtils;
/**
* An option for a library update.
*
* @author Andy Wilkinson
*/
class VersionOption {
private final DependencyVersion version;
VersionOption(DependencyVersion version) {
this.version = version;
}
DependencyVersion getVersion() {
return this.version;
}
@Override
public String toString() {
return this.version.toString();
}
static final class AlignedVersionOption extends VersionOption {
private final Library alignedWith;
AlignedVersionOption(DependencyVersion version, Library alignedWith) {
super(version);
this.alignedWith = alignedWith;
}
@Override
public String toString() {
return super.toString() + " (aligned with " + this.alignedWith.getName() + " "
+ this.alignedWith.getVersion().getVersion() + ")";
}
}
static final class ResolvedVersionOption extends VersionOption {
private final List<String> missingModules;
ResolvedVersionOption(DependencyVersion version, List<String> missingModules) {
super(version);
this.missingModules = missingModules;
}
@Override
public String toString() {
if (this.missingModules.isEmpty()) {
return super.toString();
}
return super.toString() + " (some modules are missing: "
+ StringUtils.collectionToDelimitedString(this.missingModules, ", ") + ")";
}
}
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,7 +17,6 @@
package org.springframework.boot.build.bom.bomr.github; package org.springframework.boot.build.bom.bomr.github;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* Minimal API for interacting with a GitHub repository. * Minimal API for interacting with a GitHub repository.
@ -41,7 +40,7 @@ public interface GitHubRepository {
* Returns the labels in the repository. * Returns the labels in the repository.
* @return the labels * @return the labels
*/ */
Set<String> getLabels(); List<String> getLabels();
/** /**
* Returns the milestones in the repository. * Returns the milestones in the repository.

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -35,13 +35,10 @@ public class Issue {
private final String title; private final String title;
private final State state; Issue(RestTemplate rest, int number, String title) {
Issue(RestTemplate rest, int number, String title, State state) {
this.rest = rest; this.rest = rest;
this.number = number; this.number = number;
this.title = title; this.title = title;
this.state = state;
} }
public int getNumber() { public int getNumber() {
@ -52,10 +49,6 @@ public class Issue {
return this.title; return this.title;
} }
public State getState() {
return this.state;
}
/** /**
* Labels the issue with the given {@code labels}. Any existing labels are removed. * Labels the issue with the given {@code labels}. Any existing labels are removed.
* @param labels the labels to apply to the issue * @param labels the labels to apply to the issue
@ -65,30 +58,4 @@ public class Issue {
this.rest.put("issues/" + this.number + "/labels", body); this.rest.put("issues/" + this.number + "/labels", body);
} }
public enum State {
/**
* The issue is open.
*/
OPEN,
/**
* The issue is closed.
*/
CLOSED;
static State of(String state) {
if ("open".equals(state)) {
return OPEN;
}
if ("closed".equals(state)) {
return CLOSED;
}
else {
throw new IllegalArgumentException("Unknown state '" + state + "'");
}
}
}
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,8 +16,6 @@
package org.springframework.boot.build.bom.bomr.github; package org.springframework.boot.build.bom.bomr.github;
import java.time.OffsetDateTime;
/** /**
* A milestone in a {@link GitHubRepository GitHub repository}. * A milestone in a {@link GitHubRepository GitHub repository}.
* *
@ -29,12 +27,9 @@ public class Milestone {
private final int number; private final int number;
private final OffsetDateTime dueOn; Milestone(String name, int number) {
Milestone(String name, int number, OffsetDateTime dueOn) {
this.name = name; this.name = name;
this.number = number; this.number = number;
this.dueOn = dueOn;
} }
/** /**
@ -53,10 +48,6 @@ public class Milestone {
return this.number; return this.number;
} }
public OffsetDateTime getDueOn() {
return this.dueOn;
}
@Override @Override
public String toString() { public String toString() {
return this.name + " (" + this.number + ")"; return this.name + " (" + this.number + ")";

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,12 +16,17 @@
package org.springframework.boot.build.bom.bomr.github; package org.springframework.boot.build.bom.bomr.github;
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Collections;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpRequest;
import org.springframework.http.MediaType; 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.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.DefaultUriBuilderFactory;
@ -46,14 +51,19 @@ final class StandardGitHub implements GitHub {
@Override @Override
public GitHubRepository getRepository(String organization, String name) { public GitHubRepository getRepository(String organization, String name) {
RestTemplate restTemplate = new RestTemplate( RestTemplate restTemplate = new RestTemplate(
Collections.singletonList(new MappingJackson2HttpMessageConverter(new ObjectMapper()))); Arrays.asList(new MappingJackson2HttpMessageConverter(new ObjectMapper())));
restTemplate.getInterceptors().add((request, body, execution) -> { restTemplate.getInterceptors().add(new ClientHttpRequestInterceptor() {
request.getHeaders().add("User-Agent", StandardGitHub.this.username);
request.getHeaders() @Override
.add("Authorization", "Basic " + Base64.getEncoder() public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
.encodeToString((StandardGitHub.this.username + ":" + StandardGitHub.this.password).getBytes())); throws IOException {
request.getHeaders().add("Accept", MediaType.APPLICATION_JSON_VALUE); request.getHeaders().add("User-Agent", StandardGitHub.this.username);
return execution.execute(request, body); 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( UriTemplateHandler uriTemplateHandler = new DefaultUriBuilderFactory(
"https://api.github.com/repos/" + organization + "/" + name + "/"); "https://api.github.com/repos/" + organization + "/" + name + "/");

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,18 +16,13 @@
package org.springframework.boot.build.bom.bomr.github; package org.springframework.boot.build.bom.bomr.github;
import java.time.Duration;
import java.time.OffsetDateTime;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.client.HttpClientErrorException.Forbidden;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
/** /**
@ -55,30 +50,19 @@ final class StandardGitHubRepository implements GitHubRepository {
requestBody.put("labels", labels); requestBody.put("labels", labels);
} }
requestBody.put("body", body); requestBody.put("body", body);
try { ResponseEntity<Map> response = this.rest.postForEntity("issues", requestBody, Map.class);
ResponseEntity<Map> response = this.rest.postForEntity("issues", requestBody, Map.class); return (Integer) response.getBody().get("number");
// See gh-30304
sleep(Duration.ofSeconds(3));
return (Integer) response.getBody().get("number");
}
catch (RestClientException ex) {
if (ex instanceof Forbidden forbidden) {
System.out.println("Received 403 response with headers " + forbidden.getResponseHeaders());
}
throw ex;
}
} }
@Override @Override
public Set<String> getLabels() { public List<String> getLabels() {
return new HashSet<>(get("labels?per_page=100", (label) -> (String) label.get("name"))); return get("labels?per_page=100", (label) -> (String) label.get("name"));
} }
@Override @Override
public List<Milestone> getMilestones() { public List<Milestone> getMilestones() {
return get("milestones?per_page=100", (milestone) -> new Milestone((String) milestone.get("title"), return get("milestones?per_page=100",
(Integer) milestone.get("number"), (milestone) -> new Milestone((String) milestone.get("title"), (Integer) milestone.get("number")));
(milestone.get("due_on") != null) ? OffsetDateTime.parse((String) milestone.get("due_on")) : null));
} }
@Override @Override
@ -86,23 +70,14 @@ final class StandardGitHubRepository implements GitHubRepository {
return get( return get(
"issues?per_page=100&state=all&labels=" + String.join(",", labels) + "&milestone=" "issues?per_page=100&state=all&labels=" + String.join(",", labels) + "&milestone="
+ milestone.getNumber(), + milestone.getNumber(),
(issue) -> new Issue(this.rest, (Integer) issue.get("number"), (String) issue.get("title"), (issue) -> new Issue(this.rest, (Integer) issue.get("number"), (String) issue.get("title")));
Issue.State.of((String) issue.get("state"))));
} }
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
private <T> List<T> get(String name, Function<Map<String, Object>, T> mapper) { private <T> List<T> get(String name, Function<Map<String, Object>, T> mapper) {
ResponseEntity<List> response = this.rest.getForEntity(name, List.class); ResponseEntity<List> response = this.rest.getForEntity(name, List.class);
return ((List<Map<String, Object>>) response.getBody()).stream().map(mapper).toList(); List<Map<String, Object>> body = response.getBody();
} return body.stream().map(mapper).collect(Collectors.toList());
private static void sleep(Duration duration) {
try {
Thread.sleep(duration.toMillis());
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
} }
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -33,19 +33,11 @@ abstract class AbstractDependencyVersion implements DependencyVersion {
@Override @Override
public int compareTo(DependencyVersion other) { public int compareTo(DependencyVersion other) {
ComparableVersion otherComparable = (other instanceof AbstractDependencyVersion otherVersion) ComparableVersion otherComparable = (other instanceof AbstractDependencyVersion)
? otherVersion.comparableVersion : new ComparableVersion(other.toString()); ? ((AbstractDependencyVersion) other).comparableVersion : new ComparableVersion(other.toString());
return this.comparableVersion.compareTo(otherComparable); return this.comparableVersion.compareTo(otherComparable);
} }
@Override
public boolean isUpgrade(DependencyVersion candidate, boolean movingToSnapshots) {
ComparableVersion comparableCandidate = (candidate instanceof AbstractDependencyVersion)
? ((AbstractDependencyVersion) candidate).comparableVersion
: new ComparableVersion(candidate.toString());
return comparableCandidate.compareTo(this.comparableVersion) > 0;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (this == obj) { if (this == obj) {

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,15 +16,12 @@
package org.springframework.boot.build.bom.bomr.version; package org.springframework.boot.build.bom.bomr.version;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import org.apache.maven.artifact.versioning.ArtifactVersion; import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.ComparableVersion; import org.apache.maven.artifact.versioning.ComparableVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.springframework.util.StringUtils;
/** /**
* A {@link DependencyVersion} backed by an {@link ArtifactVersion}. * A {@link DependencyVersion} backed by an {@link ArtifactVersion}.
* *
@ -35,106 +32,47 @@ class ArtifactVersionDependencyVersion extends AbstractDependencyVersion {
private final ArtifactVersion artifactVersion; private final ArtifactVersion artifactVersion;
protected ArtifactVersionDependencyVersion(ArtifactVersion artifactVersion) { protected ArtifactVersionDependencyVersion(ArtifactVersion artifactVersion) {
super(new ComparableVersion(toNormalizedString(artifactVersion))); super(new ComparableVersion(artifactVersion.toString()));
this.artifactVersion = artifactVersion; this.artifactVersion = artifactVersion;
} }
private static String toNormalizedString(ArtifactVersion artifactVersion) {
String versionString = artifactVersion.toString();
if (versionString.endsWith(".RELEASE")) {
return versionString.substring(0, versionString.length() - 8);
}
if (versionString.endsWith(".BUILD-SNAPSHOT")) {
return versionString.substring(0, versionString.length() - 15) + "-SNAPSHOT";
}
return versionString;
}
protected ArtifactVersionDependencyVersion(ArtifactVersion artifactVersion, ComparableVersion comparableVersion) { protected ArtifactVersionDependencyVersion(ArtifactVersion artifactVersion, ComparableVersion comparableVersion) {
super(comparableVersion); super(comparableVersion);
this.artifactVersion = artifactVersion; this.artifactVersion = artifactVersion;
} }
@Override @Override
public boolean isSameMajor(DependencyVersion other) { public boolean isNewerThan(DependencyVersion other) {
if (other instanceof ReleaseTrainDependencyVersion) { if (other instanceof ReleaseTrainDependencyVersion) {
return false; return false;
} }
return extractArtifactVersionDependencyVersion(other).map(this::isSameMajor).orElse(true); return compareTo(other) > 0;
}
private boolean isSameMajor(ArtifactVersionDependencyVersion other) {
return this.artifactVersion.getMajorVersion() == other.artifactVersion.getMajorVersion();
} }
@Override @Override
public boolean isSameMinor(DependencyVersion other) { public boolean isSameMajorAndNewerThan(DependencyVersion other) {
if (other instanceof ReleaseTrainDependencyVersion) { if (other instanceof ReleaseTrainDependencyVersion) {
return false; return false;
} }
return extractArtifactVersionDependencyVersion(other).map(this::isSameMinor).orElse(true); return extractArtifactVersionDependencyVersion(other).map(this::isSameMajorAndNewerThan).orElse(true);
}
private boolean isSameMinor(ArtifactVersionDependencyVersion other) {
return isSameMajor(other) && this.artifactVersion.getMinorVersion() == other.artifactVersion.getMinorVersion();
}
@Override
public boolean isUpgrade(DependencyVersion candidate, boolean movingToSnapshots) {
if (!(candidate instanceof ArtifactVersionDependencyVersion)) {
return false;
}
ArtifactVersion other = ((ArtifactVersionDependencyVersion) candidate).artifactVersion;
if (this.artifactVersion.equals(other)) {
return false;
}
if (sameMajorMinorIncremental(other)) {
if (!StringUtils.hasLength(this.artifactVersion.getQualifier())
|| "RELEASE".equals(this.artifactVersion.getQualifier())) {
return false;
}
if (isSnapshot()) {
return true;
}
else if (((ArtifactVersionDependencyVersion) candidate).isSnapshot()) {
return movingToSnapshots;
}
}
return super.isUpgrade(candidate, movingToSnapshots);
} }
private boolean sameMajorMinorIncremental(ArtifactVersion other) { private boolean isSameMajorAndNewerThan(ArtifactVersionDependencyVersion other) {
return this.artifactVersion.getMajorVersion() == other.getMajorVersion() return this.artifactVersion.getMajorVersion() == other.artifactVersion.getMajorVersion() && isNewerThan(other);
&& this.artifactVersion.getMinorVersion() == other.getMinorVersion()
&& this.artifactVersion.getIncrementalVersion() == other.getIncrementalVersion();
}
private boolean isSnapshot() {
return "SNAPSHOT".equals(this.artifactVersion.getQualifier())
|| "BUILD".equals(this.artifactVersion.getQualifier());
} }
@Override @Override
public boolean isSnapshotFor(DependencyVersion candidate) { public boolean isSameMinorAndNewerThan(DependencyVersion other) {
if (!isSnapshot() || !(candidate instanceof ArtifactVersionDependencyVersion)) { if (other instanceof ReleaseTrainDependencyVersion) {
return false; return false;
} }
return sameMajorMinorIncremental(((ArtifactVersionDependencyVersion) candidate).artifactVersion); return extractArtifactVersionDependencyVersion(other).map(this::isSameMinorAndNewerThan).orElse(true);
} }
@Override private boolean isSameMinorAndNewerThan(ArtifactVersionDependencyVersion other) {
public int compareTo(DependencyVersion other) { return this.artifactVersion.getMajorVersion() == other.artifactVersion.getMajorVersion()
if (other instanceof ArtifactVersionDependencyVersion otherArtifactDependencyVersion) { && this.artifactVersion.getMinorVersion() == other.artifactVersion.getMinorVersion()
ArtifactVersion otherArtifactVersion = otherArtifactDependencyVersion.artifactVersion; && isNewerThan(other);
if ((!Objects.equals(this.artifactVersion.getQualifier(), otherArtifactVersion.getQualifier()))
&& "snapshot".equalsIgnoreCase(otherArtifactVersion.getQualifier())
&& otherArtifactVersion.getMajorVersion() == this.artifactVersion.getMajorVersion()
&& otherArtifactVersion.getMinorVersion() == this.artifactVersion.getMinorVersion()
&& otherArtifactVersion.getIncrementalVersion() == this.artifactVersion.getIncrementalVersion()) {
return 1;
}
}
return super.compareTo(other);
} }
@Override @Override
@ -145,8 +83,8 @@ class ArtifactVersionDependencyVersion extends AbstractDependencyVersion {
protected Optional<ArtifactVersionDependencyVersion> extractArtifactVersionDependencyVersion( protected Optional<ArtifactVersionDependencyVersion> extractArtifactVersionDependencyVersion(
DependencyVersion other) { DependencyVersion other) {
ArtifactVersionDependencyVersion artifactVersion = null; ArtifactVersionDependencyVersion artifactVersion = null;
if (other instanceof ArtifactVersionDependencyVersion otherVersion) { if (other instanceof ArtifactVersionDependencyVersion) {
artifactVersion = otherVersion; artifactVersion = (ArtifactVersionDependencyVersion) other;
} }
return Optional.ofNullable(artifactVersion); return Optional.ofNullable(artifactVersion);
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,6 +41,14 @@ class CalendarVersionDependencyVersion extends ArtifactVersionDependencyVersion
super(artifactVersion, comparableVersion); super(artifactVersion, comparableVersion);
} }
@Override
public boolean isNewerThan(DependencyVersion other) {
if (other instanceof ReleaseTrainDependencyVersion) {
return true;
}
return super.isNewerThan(other);
}
static CalendarVersionDependencyVersion parse(String version) { static CalendarVersionDependencyVersion parse(String version) {
if (!CALENDAR_VERSION_PATTERN.matcher(version).matches()) { if (!CALENDAR_VERSION_PATTERN.matcher(version).matches()) {
return null; return null;

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,43 +28,34 @@ import java.util.function.Function;
public interface DependencyVersion extends Comparable<DependencyVersion> { public interface DependencyVersion extends Comparable<DependencyVersion> {
/** /**
* Returns whether this version has the same major and minor versions as the * Returns whether this version is newer than the given {@code other} version.
* {@code other} version. * @param other version to test
* @param other the version to test * @return {@code true} if this version is newer, otherwise {@code false}
* @return {@code true} if this version has the same major and minor, otherwise
* {@code false}
*/ */
boolean isSameMinor(DependencyVersion other); boolean isNewerThan(DependencyVersion other);
/** /**
* Returns whether this version has the same major version as the {@code other} * Returns whether this version has the same major versions as the {@code other}
* version. * version while also being newer.
* @param other the version to test * @param other version to test
* @return {@code true} if this version has the same major, otherwise {@code false} * @return {@code true} if this version has the same major and is newer, otherwise
*/ * {@code false}
boolean isSameMajor(DependencyVersion other);
/**
* Returns whether the given {@code candidate} is an upgrade of this version.
* @param candidate the version to consider
* @param movingToSnapshots whether the upgrade is to be considered as part of moving
* to snaphots
* @return {@code true} if the candidate is an upgrade, otherwise false
*/ */
boolean isUpgrade(DependencyVersion candidate, boolean movingToSnapshots); boolean isSameMajorAndNewerThan(DependencyVersion other);
/** /**
* Returns whether this version is a snapshot for the given {@code candidate}. * Returns whether this version has the same major and minor versions as the
* @param candidate the version to consider * {@code other} version while also being newer.
* @return {@code true} if this version is a snapshot for the candidate, otherwise * @param other version to test
* false * @return {@code true} if this version has the same major and minor and is newer,
* otherwise {@code false}
*/ */
boolean isSnapshotFor(DependencyVersion candidate); boolean isSameMinorAndNewerThan(DependencyVersion other);
static DependencyVersion parse(String version) { static DependencyVersion parse(String version) {
List<Function<String, DependencyVersion>> parsers = Arrays.asList(CalendarVersionDependencyVersion::parse, List<Function<String, DependencyVersion>> parsers = Arrays.asList(CalendarVersionDependencyVersion::parse,
ArtifactVersionDependencyVersion::parse, ReleaseTrainDependencyVersion::parse, ArtifactVersionDependencyVersion::parse, ReleaseTrainDependencyVersion::parse,
MultipleComponentsDependencyVersion::parse, CombinedPatchAndQualifierDependencyVersion::parse, NumericQualifierDependencyVersion::parse, CombinedPatchAndQualifierDependencyVersion::parse,
LeadingZeroesDependencyVersion::parse, UnstructuredDependencyVersion::parse); LeadingZeroesDependencyVersion::parse, UnstructuredDependencyVersion::parse);
for (Function<String, DependencyVersion> parser : parsers) { for (Function<String, DependencyVersion> parser : parsers) {
DependencyVersion result = parser.apply(version); DependencyVersion result = parser.apply(version);

@ -1,58 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.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 or five components
* that cannot be handled by {@link ArtifactVersion} because the fourth component is
* numeric.
*
* @author Andy Wilkinson
* @author Moritz Halbritter
*/
final class MultipleComponentsDependencyVersion extends ArtifactVersionDependencyVersion {
private final String original;
private MultipleComponentsDependencyVersion(ArtifactVersion artifactVersion, String original) {
super(artifactVersion, new ComparableVersion(original));
this.original = original;
}
@Override
public String toString() {
return this.original;
}
static MultipleComponentsDependencyVersion parse(String input) {
String[] components = input.split("\\.");
if (components.length == 4 || components.length == 5) {
ArtifactVersion artifactVersion = new DefaultArtifactVersion(
components[0] + "." + components[1] + "." + components[2]);
if (artifactVersion.getQualifier() != null && artifactVersion.getQualifier().equals(input)) {
return null;
}
return new MultipleComponentsDependencyVersion(artifactVersion, input);
}
return null;
}
}

@ -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;
}
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,8 +28,7 @@ import org.springframework.util.StringUtils;
*/ */
final class ReleaseTrainDependencyVersion implements DependencyVersion { final class ReleaseTrainDependencyVersion implements DependencyVersion {
private static final Pattern VERSION_PATTERN = Pattern private static final Pattern VERSION_PATTERN = Pattern.compile("([A-Z][a-z]+)-([A-Z]+)([0-9]*)");
.compile("([A-Z][a-z]+)-((BUILD-SNAPSHOT)|([A-Z-]+)([0-9]*))");
private final String releaseTrain; private final String releaseTrain;
@ -48,9 +47,10 @@ final class ReleaseTrainDependencyVersion implements DependencyVersion {
@Override @Override
public int compareTo(DependencyVersion other) { public int compareTo(DependencyVersion other) {
if (!(other instanceof ReleaseTrainDependencyVersion otherReleaseTrain)) { if (!(other instanceof ReleaseTrainDependencyVersion)) {
return -1; return -1;
} }
ReleaseTrainDependencyVersion otherReleaseTrain = (ReleaseTrainDependencyVersion) other;
int comparison = this.releaseTrain.compareTo(otherReleaseTrain.releaseTrain); int comparison = this.releaseTrain.compareTo(otherReleaseTrain.releaseTrain);
if (comparison != 0) { if (comparison != 0) {
return comparison; return comparison;
@ -63,56 +63,32 @@ final class ReleaseTrainDependencyVersion implements DependencyVersion {
} }
@Override @Override
public boolean isUpgrade(DependencyVersion candidate, boolean movingToSnapshots) { public boolean isNewerThan(DependencyVersion other) {
if (!(candidate instanceof ReleaseTrainDependencyVersion)) { if (other instanceof CalendarVersionDependencyVersion) {
return true; return false;
}
ReleaseTrainDependencyVersion candidateReleaseTrain = (ReleaseTrainDependencyVersion) candidate;
int comparison = this.releaseTrain.compareTo(candidateReleaseTrain.releaseTrain);
if (comparison != 0) {
return comparison < 0;
} }
if (movingToSnapshots && !isSnapshot() && candidateReleaseTrain.isSnapshot()) { if (!(other instanceof ReleaseTrainDependencyVersion)) {
return true; return true;
} }
comparison = this.type.compareTo(candidateReleaseTrain.type); ReleaseTrainDependencyVersion otherReleaseTrain = (ReleaseTrainDependencyVersion) other;
if (comparison != 0) { return otherReleaseTrain.compareTo(this) < 0;
return comparison < 0;
}
return Integer.compare(this.version, candidateReleaseTrain.version) < 0;
}
private boolean isSnapshot() {
return "BUILD-SNAPSHOT".equals(this.type);
}
@Override
public boolean isSnapshotFor(DependencyVersion candidate) {
if (!isSnapshot() || !(candidate instanceof ReleaseTrainDependencyVersion)) {
return false;
}
ReleaseTrainDependencyVersion candidateReleaseTrain = (ReleaseTrainDependencyVersion) candidate;
return this.releaseTrain.equals(candidateReleaseTrain.releaseTrain);
} }
@Override @Override
public boolean isSameMajor(DependencyVersion other) { public boolean isSameMajorAndNewerThan(DependencyVersion other) {
return isSameReleaseTrain(other); return isNewerThan(other);
} }
@Override @Override
public boolean isSameMinor(DependencyVersion other) { public boolean isSameMinorAndNewerThan(DependencyVersion other) {
return isSameReleaseTrain(other);
}
private boolean isSameReleaseTrain(DependencyVersion other) {
if (other instanceof CalendarVersionDependencyVersion) { if (other instanceof CalendarVersionDependencyVersion) {
return false; return false;
} }
if (other instanceof ReleaseTrainDependencyVersion otherReleaseTrain) { if (!(other instanceof ReleaseTrainDependencyVersion)) {
return otherReleaseTrain.releaseTrain.equals(this.releaseTrain); return true;
} }
return true; ReleaseTrainDependencyVersion otherReleaseTrain = (ReleaseTrainDependencyVersion) other;
return otherReleaseTrain.releaseTrain.equals(this.releaseTrain) && isNewerThan(other);
} }
@Override @Override
@ -148,9 +124,8 @@ final class ReleaseTrainDependencyVersion implements DependencyVersion {
if (!matcher.matches()) { if (!matcher.matches()) {
return null; return null;
} }
return new ReleaseTrainDependencyVersion(matcher.group(1), return new ReleaseTrainDependencyVersion(matcher.group(1), matcher.group(2),
StringUtils.hasLength(matcher.group(3)) ? matcher.group(3) : matcher.group(4), (StringUtils.hasLength(matcher.group(3))) ? Integer.parseInt(matcher.group(3)) : 0, input);
(StringUtils.hasLength(matcher.group(5))) ? Integer.parseInt(matcher.group(5)) : 0, input);
} }
} }

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -34,23 +34,23 @@ final class UnstructuredDependencyVersion extends AbstractDependencyVersion impl
} }
@Override @Override
public boolean isSameMajor(DependencyVersion other) { public boolean isNewerThan(DependencyVersion other) {
return true; return compareTo(other) > 0;
} }
@Override @Override
public boolean isSameMinor(DependencyVersion other) { public boolean isSameMajorAndNewerThan(DependencyVersion other) {
return true; return compareTo(other) > 0;
} }
@Override @Override
public String toString() { public boolean isSameMinorAndNewerThan(DependencyVersion other) {
return this.version; return compareTo(other) > 0;
} }
@Override @Override
public boolean isSnapshotFor(DependencyVersion candidate) { public String toString() {
return false; return this.version;
} }
static UnstructuredDependencyVersion parse(String version) { static UnstructuredDependencyVersion parse(String version) {

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -34,7 +34,6 @@ import java.util.function.Predicate;
import java.util.jar.JarEntry; import java.util.jar.JarEntry;
import java.util.jar.JarFile; import java.util.jar.JarFile;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.gradle.api.DefaultTask; import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException; import org.gradle.api.GradleException;
@ -69,10 +68,8 @@ public class CheckClasspathForConflicts extends DefaultTask {
for (File file : this.classpath) { for (File file : this.classpath) {
if (file.isDirectory()) { if (file.isDirectory()) {
Path root = file.toPath(); Path root = file.toPath();
try (Stream<Path> pathStream = Files.walk(root)) { Files.walk(root).filter((path) -> Files.isRegularFile(path))
pathStream.filter(Files::isRegularFile)
.forEach((entry) -> classpathContents.add(root.relativize(entry).toString(), root.toString())); .forEach((entry) -> classpathContents.add(root.relativize(entry).toString(), root.toString()));
}
} }
else { else {
try (JarFile jar = new JarFile(file)) { try (JarFile jar = new JarFile(file)) {
@ -111,11 +108,9 @@ public class CheckClasspathForConflicts extends DefaultTask {
} }
private Map<String, List<String>> getConflicts(List<Predicate<String>> ignores) { private Map<String, List<String>> getConflicts(List<Predicate<String>> ignores) {
return this.classpathContents.entrySet() return this.classpathContents.entrySet().stream().filter((entry) -> entry.getValue().size() > 1)
.stream() .filter((entry) -> canConflict(entry.getKey(), ignores))
.filter((entry) -> entry.getValue().size() > 1) .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v1, TreeMap::new));
.filter((entry) -> canConflict(entry.getKey(), ignores))
.collect(Collectors.toMap(Entry::getKey, Entry::getValue, (v1, v2) -> v1, TreeMap::new));
} }
private boolean canConflict(String name, List<Predicate<String>> ignores) { private boolean canConflict(String name, List<Predicate<String>> ignores) {

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.boot.build.classpath; package org.springframework.boot.build.classpath;
import java.io.IOException;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -51,14 +52,10 @@ public class CheckClasspathForProhibitedDependencies extends DefaultTask {
} }
@TaskAction @TaskAction
public void checkForProhibitedDependencies() { public void checkForProhibitedDependencies() throws IOException {
TreeSet<String> prohibited = this.classpath.getResolvedConfiguration() TreeSet<String> prohibited = this.classpath.getResolvedConfiguration().getResolvedArtifacts().stream()
.getResolvedArtifacts() .map((artifact) -> artifact.getModuleVersion().getId()).filter(this::prohibited)
.stream() .map((id) -> id.getGroup() + ":" + id.getName()).collect(Collectors.toCollection(TreeSet::new));
.map((artifact) -> artifact.getModuleVersion().getId())
.filter(this::prohibited)
.map((id) -> id.getGroup() + ":" + id.getName())
.collect(Collectors.toCollection(TreeSet::new));
if (!prohibited.isEmpty()) { if (!prohibited.isEmpty()) {
StringBuilder message = new StringBuilder(String.format("Found prohibited dependencies:%n")); StringBuilder message = new StringBuilder(String.format("Found prohibited dependencies:%n"));
for (String dependency : prohibited) { for (String dependency : prohibited) {
@ -79,12 +76,6 @@ public class CheckClasspathForProhibitedDependencies extends DefaultTask {
if (group.equals("javax.money")) { if (group.equals("javax.money")) {
return false; return false;
} }
if (group.equals("org.codehaus.groovy")) {
return true;
}
if (group.equals("org.eclipse.jetty.toolchain")) {
return true;
}
if (group.startsWith("javax")) { if (group.startsWith("javax")) {
return true; return true;
} }
@ -100,9 +91,6 @@ public class CheckClasspathForProhibitedDependencies extends DefaultTask {
if (group.equals("org.apache.geronimo.specs")) { if (group.equals("org.apache.geronimo.specs")) {
return true; return true;
} }
if (group.equals("com.sun.activation")) {
return true;
}
return false; return false;
} }

@ -1,78 +0,0 @@
/*
* Copyright 2023-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.classpath;
import java.util.Set;
import java.util.stream.Collectors;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.component.ModuleComponentSelector;
import org.gradle.api.artifacts.result.DependencyResult;
import org.gradle.api.artifacts.result.ResolutionResult;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.TaskAction;
/**
* Tasks to check that none of classpath's direct dependencies are unconstrained.
*
* @author Andy Wilkinson
*/
public class CheckClasspathForUnconstrainedDirectDependencies extends DefaultTask {
private Configuration classpath;
public CheckClasspathForUnconstrainedDirectDependencies() {
getOutputs().upToDateWhen((task) -> true);
}
@Classpath
public FileCollection getClasspath() {
return this.classpath;
}
public void setClasspath(Configuration classpath) {
this.classpath = classpath;
}
@TaskAction
void checkForUnconstrainedDirectDependencies() {
ResolutionResult resolutionResult = this.classpath.getIncoming().getResolutionResult();
Set<? extends DependencyResult> dependencies = resolutionResult.getRoot().getDependencies();
Set<String> unconstrainedDependencies = dependencies.stream()
.map(DependencyResult::getRequested)
.filter(ModuleComponentSelector.class::isInstance)
.map(ModuleComponentSelector.class::cast)
.map((selector) -> selector.getGroup() + ":" + selector.getModule())
.collect(Collectors.toSet());
Set<String> constraints = resolutionResult.getAllDependencies()
.stream()
.filter(DependencyResult::isConstraint)
.map(DependencyResult::getRequested)
.filter(ModuleComponentSelector.class::isInstance)
.map(ModuleComponentSelector.class::cast)
.map((selector) -> selector.getGroup() + ":" + selector.getModule())
.collect(Collectors.toSet());
unconstrainedDependencies.removeAll(constraints);
if (!unconstrainedDependencies.isEmpty()) {
throw new GradleException("Found unconstrained direct dependencies: " + unconstrainedDependencies);
}
}
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,6 +18,7 @@ package org.springframework.boot.build.classpath;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
@ -38,8 +39,6 @@ import org.gradle.api.artifacts.ModuleDependency;
import org.gradle.api.artifacts.component.ModuleComponentIdentifier; import org.gradle.api.artifacts.component.ModuleComponentIdentifier;
import org.gradle.api.artifacts.dsl.DependencyHandler; import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.artifacts.result.ResolvedArtifactResult; import org.gradle.api.artifacts.result.ResolvedArtifactResult;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.Classpath;
import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
@ -63,42 +62,32 @@ public class CheckClasspathForUnnecessaryExclusions extends DefaultTask {
private final ConfigurationContainer configurations; private final ConfigurationContainer configurations;
private Configuration classpath;
@Inject @Inject
public CheckClasspathForUnnecessaryExclusions(DependencyHandler dependencyHandler, public CheckClasspathForUnnecessaryExclusions(DependencyHandler dependencyHandler,
ConfigurationContainer configurations) { ConfigurationContainer configurations) {
this.dependencyHandler = getProject().getDependencies(); this.dependencyHandler = getProject().getDependencies();
this.configurations = getProject().getConfigurations(); this.configurations = getProject().getConfigurations();
this.platform = this.dependencyHandler this.platform = this.dependencyHandler.create(
.create(this.dependencyHandler.platform(this.dependencyHandler.project(SPRING_BOOT_DEPENDENCIES_PROJECT))); this.dependencyHandler.platform(this.dependencyHandler.project(SPRING_BOOT_DEPENDENCIES_PROJECT)));
getOutputs().upToDateWhen((task) -> true); getOutputs().upToDateWhen((task) -> true);
} }
public void setClasspath(Configuration classpath) { public void setClasspath(Configuration classpath) {
this.classpath = classpath;
this.exclusionsByDependencyId.clear(); this.exclusionsByDependencyId.clear();
this.dependencyById.clear(); this.dependencyById.clear();
classpath.getAllDependencies().all(this::processDependency); classpath.getAllDependencies().all(this::processDependency);
} }
@Classpath
public FileCollection getClasspath() {
return this.classpath;
}
private void processDependency(Dependency dependency) { private void processDependency(Dependency dependency) {
if (dependency instanceof ModuleDependency moduleDependency) { if (dependency instanceof ModuleDependency) {
processDependency(moduleDependency); processDependency((ModuleDependency) dependency);
} }
} }
private void processDependency(ModuleDependency dependency) { private void processDependency(ModuleDependency dependency) {
String dependencyId = getId(dependency); String dependencyId = getId(dependency);
TreeSet<String> exclusions = dependency.getExcludeRules() TreeSet<String> exclusions = dependency.getExcludeRules().stream().map(this::getId)
.stream() .collect(Collectors.toCollection(TreeSet::new));
.map(this::getId)
.collect(Collectors.toCollection(TreeSet::new));
this.exclusionsByDependencyId.put(dependencyId, exclusions); this.exclusionsByDependencyId.put(dependencyId, exclusions);
if (!exclusions.isEmpty()) { if (!exclusions.isEmpty()) {
this.dependencyById.put(dependencyId, getProject().getDependencies().create(dependencyId)); this.dependencyById.put(dependencyId, getProject().getDependencies().create(dependencyId));
@ -116,13 +105,10 @@ public class CheckClasspathForUnnecessaryExclusions extends DefaultTask {
this.exclusionsByDependencyId.forEach((dependencyId, exclusions) -> { this.exclusionsByDependencyId.forEach((dependencyId, exclusions) -> {
if (!exclusions.isEmpty()) { if (!exclusions.isEmpty()) {
Dependency toCheck = this.dependencyById.get(dependencyId); Dependency toCheck = this.dependencyById.get(dependencyId);
this.configurations.detachedConfiguration(toCheck, this.platform) List<String> dependencies = this.configurations.detachedConfiguration(toCheck, this.platform)
.getIncoming() .getIncoming().getArtifacts().getArtifacts().stream().map(this::getId)
.getArtifacts() .collect(Collectors.toList());
.getArtifacts() exclusions.removeAll(dependencies);
.stream()
.map(this::getId)
.forEach(exclusions::remove);
removeProfileExclusions(dependencyId, exclusions); removeProfileExclusions(dependencyId, exclusions);
if (!exclusions.isEmpty()) { if (!exclusions.isEmpty()) {
unnecessaryExclusions.put(dependencyId, exclusions); unnecessaryExclusions.put(dependencyId, exclusions);

@ -0,0 +1,105 @@
/*
* Copyright 2012-2021 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.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.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskExecutionException;
import org.springframework.boot.build.artifactory.ArtifactoryRepository;
/**
* 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<RegularFile> archive;
private File template;
private File outputDir;
public AbstractPackageManagerDefinitionTask() {
getInputs().property("version", getProject().provider(getProject()::getVersion));
}
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
public RegularFile getArchive() {
return this.archive.get();
}
public void setArchive(Provider<RegularFile> archive) {
this.archive = archive;
}
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
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<String, Object> additionalProperties) {
getProject().copy((copy) -> {
copy.from(this.template);
copy.into(this.outputDir);
Map<String, Object> properties = new HashMap<>(additionalProperties);
properties.put("hash", sha256(this.archive.get().getAsFile()));
properties.put("repo", ArtifactoryRepository.forProject(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);
}
}
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,99 +16,17 @@
package org.springframework.boot.build.cli; package org.springframework.boot.build.cli;
import java.io.File;
import java.security.MessageDigest;
import java.util.Collections; import java.util.Collections;
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.Task; import org.gradle.api.Task;
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.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskExecutionException;
import org.springframework.boot.build.artifacts.ArtifactRelease;
/** /**
* A {@link Task} for creating a Homebrew formula manifest. * A {@link Task} for creating a Homebrew formula manifest.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
public class HomebrewFormula extends DefaultTask { public class HomebrewFormula extends AbstractPackageManagerDefinitionTask {
private Provider<RegularFile> archive;
private File template;
private File outputDir;
public HomebrewFormula() {
getInputs().property("version", getProject().provider(getProject()::getVersion));
}
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
public RegularFile getArchive() {
return this.archive.get();
}
public void setArchive(Provider<RegularFile> archive) {
this.archive = archive;
}
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
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<String, Object> additionalProperties) {
getProject().copy((copy) -> {
copy.from(this.template);
copy.into(this.outputDir);
copy.expand(getProperties(additionalProperties));
});
}
private Map<String, Object> getProperties(Map<String, Object> additionalProperties) {
Map<String, Object> properties = new HashMap<>(additionalProperties);
Project project = getProject();
properties.put("hash", sha256(this.archive.get().getAsFile()));
properties.put("repo", ArtifactRelease.forProject(project).getDownloadRepo());
properties.put("project", project);
return 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);
}
}
@TaskAction @TaskAction
void createFormula() { void createFormula() {

@ -0,0 +1,37 @@
/*
* 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.Task;
import org.gradle.api.tasks.TaskAction;
/**
* A {@link 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('.'))));
}
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -64,10 +64,8 @@ public class ExtractVersionConstraints extends DefaultTask {
} }
public void enforcedPlatform(String projectPath) { public void enforcedPlatform(String projectPath) {
this.configuration.getDependencies() this.configuration.getDependencies().add(getProject().getDependencies().enforcedPlatform(
.add(getProject().getDependencies() getProject().getDependencies().project(Collections.singletonMap("path", projectPath))));
.enforcedPlatform(
getProject().getDependencies().project(Collections.singletonMap("path", projectPath))));
this.projectPaths.add(projectPath); this.projectPaths.add(projectPath);
} }
@ -91,10 +89,8 @@ public class ExtractVersionConstraints extends DefaultTask {
this.configuration.resolve(); this.configuration.resolve();
for (String projectPath : this.projectPaths) { for (String projectPath : this.projectPaths) {
extractVersionProperties(projectPath); extractVersionProperties(projectPath);
for (DependencyConstraint constraint : getProject().project(projectPath) for (DependencyConstraint constraint : getProject().project(projectPath).getConfigurations()
.getConfigurations() .getByName("apiElements").getAllDependencyConstraints()) {
.getByName("apiElements")
.getAllDependencyConstraints()) {
this.versionConstraints.put(constraint.getGroup() + ":" + constraint.getName(), this.versionConstraints.put(constraint.getGroup() + ":" + constraint.getName(),
constraint.getVersionConstraint().toString()); constraint.getVersionConstraint().toString());
this.constrainedVersions.add(new ConstrainedVersion(constraint.getGroup(), constraint.getName(), this.constrainedVersions.add(new ConstrainedVersion(constraint.getGroup(), constraint.getName(),

@ -1,59 +0,0 @@
/*
* Copyright 2012-2021 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 Asciidoc {
private final StringBuilder content;
Asciidoc() {
this.content = new StringBuilder();
}
Asciidoc appendWithHardLineBreaks(Object... items) {
for (Object item : items) {
appendln("`+", item, "+` +");
}
return this;
}
Asciidoc appendln(Object... items) {
return append(items).newLine();
}
Asciidoc append(Object... items) {
for (Object item : items) {
this.content.append(item);
}
return this;
}
Asciidoc newLine() {
return append(System.lineSeparator());
}
@Override
public String toString() {
return this.content.toString();
}
}

@ -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();
}
}

@ -1,165 +0,0 @@
/*
* Copyright 2012-2022 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 java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.gradle.api.GradleException;
import org.gradle.api.file.FileTree;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SourceTask;
import org.gradle.api.tasks.TaskAction;
/**
* {@link SourceTask} that checks additional Spring configuration metadata files.
*
* @author Andy Wilkinson
*/
public class CheckAdditionalSpringConfigurationMetadata extends SourceTask {
private final RegularFileProperty reportLocation;
public CheckAdditionalSpringConfigurationMetadata() {
this.reportLocation = getProject().getObjects().fileProperty();
}
@OutputFile
public RegularFileProperty getReportLocation() {
return this.reportLocation;
}
@Override
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
public FileTree getSource() {
return super.getSource();
}
@TaskAction
void check() throws JsonParseException, IOException {
Report report = createReport();
File reportFile = getReportLocation().get().getAsFile();
Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
if (report.hasProblems()) {
throw new GradleException(
"Problems found in additional Spring configuration metadata. See " + reportFile + " for details.");
}
}
@SuppressWarnings("unchecked")
private Report createReport() throws IOException, JsonParseException, JsonMappingException {
ObjectMapper objectMapper = new ObjectMapper();
Report report = new Report();
for (File file : getSource().getFiles()) {
Analysis analysis = report.analysis(getProject().getProjectDir().toPath().relativize(file.toPath()));
Map<String, Object> json = objectMapper.readValue(file, Map.class);
check("groups", json, analysis);
check("properties", json, analysis);
check("hints", json, analysis);
}
return report;
}
@SuppressWarnings("unchecked")
private void check(String key, Map<String, Object> json, Analysis analysis) {
List<Map<String, Object>> groups = (List<Map<String, Object>>) json.get(key);
List<String> names = groups.stream().map((group) -> (String) group.get("name")).toList();
List<String> sortedNames = sortedCopy(names);
for (int i = 0; i < names.size(); i++) {
String actual = names.get(i);
String expected = sortedNames.get(i);
if (!actual.equals(expected)) {
analysis.problems.add("Wrong order at $." + key + "[" + i + "].name - expected '" + expected
+ "' but found '" + actual + "'");
}
}
}
private List<String> sortedCopy(Collection<String> original) {
List<String> copy = new ArrayList<>(original);
Collections.sort(copy);
return copy;
}
private static final class Report implements Iterable<String> {
private final List<Analysis> analyses = new ArrayList<>();
private Analysis analysis(Path path) {
Analysis analysis = new Analysis(path);
this.analyses.add(analysis);
return analysis;
}
private boolean hasProblems() {
for (Analysis analysis : this.analyses) {
if (!analysis.problems.isEmpty()) {
return true;
}
}
return false;
}
@Override
public Iterator<String> iterator() {
List<String> lines = new ArrayList<>();
for (Analysis analysis : this.analyses) {
lines.add(analysis.source.toString());
lines.add("");
if (analysis.problems.isEmpty()) {
lines.add("No problems found.");
}
else {
lines.addAll(analysis.problems);
}
lines.add("");
}
return lines.iterator();
}
}
private static final class Analysis {
private final List<String> problems = new ArrayList<>();
private final Path source;
private Analysis(Path source) {
this.source = source;
}
}
}

@ -1,165 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.context.properties;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputFile;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SourceTask;
import org.gradle.api.tasks.TaskAction;
/**
* {@link SourceTask} that checks {@code spring-configuration-metadata.json} files.
*
* @author Andy Wilkinson
*/
public class CheckSpringConfigurationMetadata extends DefaultTask {
private List<String> exclusions = new ArrayList<>();
private final RegularFileProperty reportLocation;
private final RegularFileProperty metadataLocation;
public CheckSpringConfigurationMetadata() {
this.metadataLocation = getProject().getObjects().fileProperty();
this.reportLocation = getProject().getObjects().fileProperty();
}
@OutputFile
public RegularFileProperty getReportLocation() {
return this.reportLocation;
}
@InputFile
@PathSensitive(PathSensitivity.RELATIVE)
public RegularFileProperty getMetadataLocation() {
return this.metadataLocation;
}
public void setExclusions(List<String> exclusions) {
this.exclusions = exclusions;
}
@Input
public List<String> getExclusions() {
return this.exclusions;
}
@TaskAction
void check() throws JsonParseException, IOException {
Report report = createReport();
File reportFile = getReportLocation().get().getAsFile();
Files.write(reportFile.toPath(), report, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
if (report.hasProblems()) {
throw new GradleException(
"Problems found in Spring configuration metadata. See " + reportFile + " for details.");
}
}
@SuppressWarnings("unchecked")
private Report createReport() throws IOException, JsonParseException, JsonMappingException {
ObjectMapper objectMapper = new ObjectMapper();
File file = this.metadataLocation.get().getAsFile();
Report report = new Report(getProject().getProjectDir().toPath().relativize(file.toPath()));
Map<String, Object> json = objectMapper.readValue(file, Map.class);
List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
for (Map<String, Object> property : properties) {
String name = (String) property.get("name");
if (!isDeprecated(property) && !isDescribed(property) && !isExcluded(name)) {
report.propertiesWithNoDescription.add(name);
}
}
return report;
}
private boolean isExcluded(String propertyName) {
for (String exclusion : this.exclusions) {
if (propertyName.equals(exclusion)) {
return true;
}
if (exclusion.endsWith(".*")) {
if (propertyName.startsWith(exclusion.substring(0, exclusion.length() - 2))) {
return true;
}
}
}
return false;
}
@SuppressWarnings("unchecked")
private boolean isDeprecated(Map<String, Object> property) {
return (Map<String, Object>) property.get("deprecation") != null;
}
private boolean isDescribed(Map<String, Object> property) {
return property.get("description") != null;
}
private static final class Report implements Iterable<String> {
private final List<String> propertiesWithNoDescription = new ArrayList<>();
private final Path source;
private Report(Path source) {
this.source = source;
}
private boolean hasProblems() {
return !this.propertiesWithNoDescription.isEmpty();
}
@Override
public Iterator<String> iterator() {
List<String> lines = new ArrayList<>();
lines.add(this.source.toString());
lines.add("");
if (this.propertiesWithNoDescription.isEmpty()) {
lines.add("No problems found.");
}
else {
lines.add("The following properties have no description:");
lines.add("");
lines.addAll(this.propertiesWithNoDescription.stream().map((line) -> "\t" + line).toList());
}
lines.add("");
return lines.iterator();
}
}
}

@ -0,0 +1,52 @@
/*
* 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 final Set<String> configurationKeys;
private final 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.key + "]]<<" + this.key + ",");
this.configurationKeys.forEach(builder::appendKey);
builder.appendln(">>");
builder.newLine().appendln("|").appendln("|+++", this.description, "+++");
}
}

@ -1,55 +0,0 @@
/*
* Copyright 2012-2021 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;
/**
* Table row regrouping a list of configuration properties sharing the same description.
*
* @author Brian Clozel
* @author Phillip Webb
*/
class CompoundRow extends Row {
private final Set<String> propertyNames;
private final String description;
CompoundRow(Snippet snippet, String prefix, String description) {
super(snippet, prefix);
this.description = description;
this.propertyNames = new TreeSet<>();
}
void addProperty(ConfigurationProperty property) {
this.propertyNames.add(property.getDisplayName());
}
@Override
void write(Asciidoc asciidoc) {
asciidoc.append("|");
asciidoc.append("[[" + getAnchor() + "]]");
asciidoc.append("<<" + getAnchor() + ",");
this.propertyNames.forEach(asciidoc::appendWithHardLineBreaks);
asciidoc.appendln(">>");
asciidoc.appendln("|+++", this.description, "+++");
asciidoc.appendln("|");
}
}

@ -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<ConfigurationTable> 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<ConfigurationTable> createConfigTables(Map<String, ConfigurationProperty> metadataProperties,
DocumentOptions options) {
List<ConfigurationTable> tables = new ArrayList<>();
List<String> unmappedKeys = metadataProperties.values().stream().filter((property) -> !property.isDeprecated())
.map(ConfigurationProperty::getName).collect(Collectors.toList());
Map<String, CompoundConfigurationTableEntry> 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<String, CompoundConfigurationTableEntry> getOverrides(
Map<String, ConfigurationProperty> metadataProperties, List<String> unmappedKeys, DocumentOptions options) {
Map<String, CompoundConfigurationTableEntry> overrides = new HashMap<>();
options.getOverrides().forEach((keyPrefix, description) -> {
CompoundConfigurationTableEntry entry = new CompoundConfigurationTableEntry(keyPrefix, description);
List<String> 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<String, ConfigurationProperty> metadataProperties,
List<String> unmappedKeys, Map<String, CompoundConfigurationTableEntry> overrides, String id,
List<String> keyPrefixes) {
ConfigurationTable table = new ConfigurationTable(id);
for (String keyPrefix : keyPrefixes) {
List<String> matchingOverrides = overrides.keySet().stream()
.filter((overrideKey) -> overrideKey.startsWith(keyPrefix)).collect(Collectors.toList());
matchingOverrides.forEach((match) -> table.addEntry(overrides.remove(match)));
}
List<String> 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));
}
}
}

@ -17,13 +17,14 @@
package org.springframework.boot.build.context.properties; package org.springframework.boot.build.context.properties;
import java.io.File; import java.io.File;
import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream; import java.util.function.Function;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@ -32,40 +33,35 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* {@code META-INF/spring-configuration-metadata.json} files. * {@code META-INF/spring-configuration-metadata.json} files.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb
*/ */
final class ConfigurationProperties { final class ConfigurationProperties {
private final Map<String, ConfigurationProperty> byName; private ConfigurationProperties() {
private ConfigurationProperties(List<ConfigurationProperty> properties) {
Map<String, ConfigurationProperty> byName = new LinkedHashMap<>();
for (ConfigurationProperty property : properties) {
byName.put(property.getName(), property);
}
this.byName = Collections.unmodifiableMap(byName);
}
ConfigurationProperty get(String propertyName) {
return this.byName.get(propertyName);
}
Stream<ConfigurationProperty> stream() {
return this.byName.values().stream();
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
static ConfigurationProperties fromFiles(Iterable<File> files) { static Map<String, ConfigurationProperty> fromFiles(Iterable<File> files) {
List<ConfigurationProperty> configurationProperties = new ArrayList<>();
try { try {
ObjectMapper objectMapper = new ObjectMapper(); ObjectMapper objectMapper = new ObjectMapper();
List<ConfigurationProperty> properties = new ArrayList<>();
for (File file : files) { for (File file : files) {
Map<String, Object> json = objectMapper.readValue(file, Map.class); try (Reader reader = new FileReader(file)) {
for (Map<String, Object> property : (List<Map<String, Object>>) json.get("properties")) { Map<String, Object> json = objectMapper.readValue(file, Map.class);
properties.add(ConfigurationProperty.fromJsonProperties(property)); List<Map<String, Object>> properties = (List<Map<String, Object>>) json.get("properties");
for (Map<String, Object> 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 new ConfigurationProperties(properties); return configurationProperties.stream()
.collect(Collectors.toMap(ConfigurationProperty::getName, Function.identity()));
} }
catch (IOException ex) { catch (IOException ex) {
throw new RuntimeException("Failed to load configuration metadata", ex); throw new RuntimeException("Failed to load configuration metadata", ex);

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2021 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,24 +16,21 @@
package org.springframework.boot.build.context.properties; package org.springframework.boot.build.context.properties;
import java.io.File;
import java.util.Collections; import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.gradle.api.Plugin; import org.gradle.api.Plugin;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.Task; import org.gradle.api.Task;
import org.gradle.api.artifacts.Configuration; import org.gradle.api.artifacts.Configuration;
import org.gradle.api.file.RegularFile;
import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.compile.JavaCompile; import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
import org.springframework.boot.build.processors.ProcessedAnnotationsPlugin;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@ -42,13 +39,10 @@ import org.springframework.util.StringUtils;
* *
* <ul> * <ul>
* <li>Adding a dependency on the configuration properties annotation processor. * <li>Adding a dependency on the configuration properties annotation processor.
* <li>Disables incremental compilation to avoid property descriptions being lost.
* <li>Configuring the additional metadata locations annotation processor compiler * <li>Configuring the additional metadata locations annotation processor compiler
* argument. * argument.
* <li>Adding the outputs of the processResources task as inputs of the compileJava task * <li>Adding the outputs of the processResources task as inputs of the compileJava task
* to ensure that the additional metadata is available when the annotation processor runs. * to ensure that the additional metadata is available when the annotation processor runs.
* <li>Registering a {@link CheckAdditionalSpringConfigurationMetadata} task and
* configuring the {@code check} task to depend upon it.
* <li>Defining an artifact for the resulting configuration property metadata so that it * <li>Defining an artifact for the resulting configuration property metadata so that it
* can be consumed by downstream projects. * can be consumed by downstream projects.
* </ul> * </ul>
@ -63,126 +57,45 @@ public class ConfigurationPropertiesPlugin implements Plugin<Project> {
*/ */
public static final String CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME = "configurationPropertiesMetadata"; public static final String CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME = "configurationPropertiesMetadata";
/**
* Name of the {@link CheckAdditionalSpringConfigurationMetadata} task.
*/
public static final String CHECK_ADDITIONAL_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkAdditionalSpringConfigurationMetadata";
/**
* Name of the {@link CheckAdditionalSpringConfigurationMetadata} task.
*/
public static final String CHECK_SPRING_CONFIGURATION_METADATA_TASK_NAME = "checkSpringConfigurationMetadata";
@Override @Override
public void apply(Project project) { public void apply(Project project) {
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> { project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
configureConfigurationPropertiesAnnotationProcessor(project); addConfigurationProcessorDependency(project);
disableIncrementalCompilation(project);
configureAdditionalMetadataLocationsCompilerArgument(project); configureAdditionalMetadataLocationsCompilerArgument(project);
registerCheckAdditionalMetadataTask(project);
registerCheckMetadataTask(project);
addMetadataArtifact(project); addMetadataArtifact(project);
}); });
} }
private void configureConfigurationPropertiesAnnotationProcessor(Project project) { private void addConfigurationProcessorDependency(Project project) {
Configuration annotationProcessors = project.getConfigurations() Configuration annotationProcessors = project.getConfigurations()
.getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME); .getByName(JavaPlugin.ANNOTATION_PROCESSOR_CONFIGURATION_NAME);
annotationProcessors.getDependencies() annotationProcessors.getDependencies().add(project.getDependencies().project(Collections.singletonMap("path",
.add(project.getDependencies() ":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor")));
.project(Collections.singletonMap("path",
":spring-boot-project:spring-boot-tools:spring-boot-configuration-processor")));
project.getPlugins().apply(ProcessedAnnotationsPlugin.class);
}
private void disableIncrementalCompilation(Project project) {
SourceSet mainSourceSet = project.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
project.getTasks()
.named(mainSourceSet.getCompileJavaTaskName(), JavaCompile.class)
.configure((compileJava) -> compileJava.getOptions().setIncremental(false));
} }
private void addMetadataArtifact(Project project) { private void addMetadataArtifact(Project project) {
SourceSet mainSourceSet = project.getExtensions() SourceSet mainSourceSet = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets()
.getByType(JavaPluginExtension.class) .getByName(SourceSet.MAIN_SOURCE_SET_NAME);
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
project.getConfigurations().maybeCreate(CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME); project.getConfigurations().maybeCreate(CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME);
project.afterEvaluate((evaluatedProject) -> evaluatedProject.getArtifacts() project.afterEvaluate((evaluatedProject) -> evaluatedProject.getArtifacts().add(
.add(CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME, CONFIGURATION_PROPERTIES_METADATA_CONFIGURATION_NAME,
mainSourceSet.getJava() evaluatedProject.provider((Callable<File>) () -> new File(mainSourceSet.getJava().getOutputDir(),
.getDestinationDirectory() "META-INF/spring-configuration-metadata.json")),
.dir("META-INF/spring-configuration-metadata.json"), (artifact) -> artifact
(artifact) -> artifact
.builtBy(evaluatedProject.getTasks().getByName(mainSourceSet.getClassesTaskName())))); .builtBy(evaluatedProject.getTasks().getByName(mainSourceSet.getClassesTaskName()))));
} }
private void configureAdditionalMetadataLocationsCompilerArgument(Project project) { private void configureAdditionalMetadataLocationsCompilerArgument(Project project) {
JavaCompile compileJava = project.getTasks() JavaCompile compileJava = project.getTasks().withType(JavaCompile.class)
.withType(JavaCompile.class) .getByName(JavaPlugin.COMPILE_JAVA_TASK_NAME);
.getByName(JavaPlugin.COMPILE_JAVA_TASK_NAME); ((Task) compileJava).getInputs().files(project.getTasks().getByName(JavaPlugin.PROCESS_RESOURCES_TASK_NAME))
((Task) compileJava).getInputs() .withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("processed resources");
.files(project.getTasks().getByName(JavaPlugin.PROCESS_RESOURCES_TASK_NAME)) SourceSet mainSourceSet = project.getConvention().getPlugin(JavaPluginConvention.class).getSourceSets()
.withPathSensitivity(PathSensitivity.RELATIVE)
.withPropertyName("processed resources");
SourceSet mainSourceSet = project.getExtensions()
.getByType(JavaPluginExtension.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())));
}
private void registerCheckAdditionalMetadataTask(Project project) {
TaskProvider<CheckAdditionalSpringConfigurationMetadata> checkConfigurationMetadata = project.getTasks()
.register(CHECK_ADDITIONAL_SPRING_CONFIGURATION_METADATA_TASK_NAME,
CheckAdditionalSpringConfigurationMetadata.class);
checkConfigurationMetadata.configure((check) -> {
SourceSet mainSourceSet = project.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME);
check.setSource(mainSourceSet.getResources());
check.include("META-INF/additional-spring-configuration-metadata.json");
check.getReportLocation()
.set(project.getLayout()
.getBuildDirectory()
.file("reports/additional-spring-configuration-metadata/check.txt"));
});
project.getTasks()
.named(LifecycleBasePlugin.CHECK_TASK_NAME)
.configure((check) -> check.dependsOn(checkConfigurationMetadata));
}
private void registerCheckMetadataTask(Project project) {
TaskProvider<CheckSpringConfigurationMetadata> checkConfigurationMetadata = project.getTasks()
.register(CHECK_SPRING_CONFIGURATION_METADATA_TASK_NAME, CheckSpringConfigurationMetadata.class);
checkConfigurationMetadata.configure((check) -> {
SourceSet mainSourceSet = project.getExtensions()
.getByType(JavaPluginExtension.class)
.getSourceSets()
.getByName(SourceSet.MAIN_SOURCE_SET_NAME); .getByName(SourceSet.MAIN_SOURCE_SET_NAME);
Provider<RegularFile> metadataLocation = project.getTasks() compileJava.getOptions().getCompilerArgs()
.named(mainSourceSet.getCompileJavaTaskName(), JavaCompile.class) .add("-Aorg.springframework.boot.configurationprocessor.additionalMetadataLocations=" + StringUtils
.flatMap((javaCompile) -> javaCompile.getDestinationDirectory() .collectionToCommaDelimitedString(mainSourceSet.getResources().getSourceDirectories().getFiles()
.file("META-INF/spring-configuration-metadata.json")); .stream().map(project.getRootProject()::relativePath).collect(Collectors.toSet())));
check.getMetadataLocation().set(metadataLocation);
check.getReportLocation()
.set(project.getLayout().getBuildDirectory().file("reports/spring-configuration-metadata/check.txt"));
});
project.getTasks()
.named(LifecycleBasePlugin.CHECK_TASK_NAME)
.configure((check) -> check.dependsOn(checkConfigurationMetadata));
} }
} }

@ -16,14 +16,12 @@
package org.springframework.boot.build.context.properties; package org.springframework.boot.build.context.properties;
import java.util.Map;
/** /**
* A configuration property. * A configuration property.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
*/ */
class ConfigurationProperty { public class ConfigurationProperty {
private final String name; private final String name;
@ -47,27 +45,23 @@ class ConfigurationProperty {
this.deprecated = deprecated; this.deprecated = deprecated;
} }
String getName() { public String getName() {
return this.name; return this.name;
} }
String getDisplayName() { public String getType() {
return (getType() != null && getType().startsWith("java.util.Map")) ? getName() + ".*" : getName();
}
String getType() {
return this.type; return this.type;
} }
Object getDefaultValue() { public Object getDefaultValue() {
return this.defaultValue; return this.defaultValue;
} }
String getDescription() { public String getDescription() {
return this.description; return this.description;
} }
boolean isDeprecated() { public boolean isDeprecated() {
return this.deprecated; return this.deprecated;
} }
@ -76,13 +70,4 @@ class ConfigurationProperty {
return "ConfigurationProperty [name=" + this.name + ", type=" + this.type + "]"; return "ConfigurationProperty [name=" + this.name + ", type=" + this.type + "]";
} }
static ConfigurationProperty fromJsonProperties(Map<String, Object> property) {
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");
return new ConfigurationProperty(name, type, defaultValue, description, deprecated);
}
} }

@ -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<ConfigurationTableEntry> 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=\"2,1,1\", 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();
}
}

@ -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<ConfigurationTableEntry> {
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());
}
}

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 the original author or authors. * Copyright 2012-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -28,13 +28,12 @@ import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity; import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
import org.springframework.boot.build.context.properties.Snippet.Config; import org.springframework.boot.build.context.properties.DocumentOptions.Builder;
/** /**
* {@link Task} used to document auto-configuration classes. * {@link Task} used to document auto-configuration classes.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb
*/ */
public class DocumentConfigurationProperties extends DefaultTask { public class DocumentConfigurationProperties extends DefaultTask {
@ -63,168 +62,42 @@ public class DocumentConfigurationProperties extends DefaultTask {
@TaskAction @TaskAction
void documentConfigurationProperties() throws IOException { void documentConfigurationProperties() throws IOException {
Snippets snippets = new Snippets(this.configurationPropertyMetadata); Builder builder = DocumentOptions.builder();
snippets.add("application-properties.core", "Core Properties", this::corePrefixes); builder.addSection("core")
snippets.add("application-properties.cache", "Cache Properties", this::cachePrefixes); .withKeyPrefixes("debug", "trace", "logging", "spring.aop", "spring.application",
snippets.add("application-properties.mail", "Mail Properties", this::mailPrefixes); "spring.autoconfigure", "spring.banner", "spring.beaninfo", "spring.codec", "spring.config",
snippets.add("application-properties.json", "JSON Properties", this::jsonPrefixes); "spring.info", "spring.jmx", "spring.lifecycle", "spring.main", "spring.messages", "spring.pid",
snippets.add("application-properties.data", "Data Properties", this::dataPrefixes); "spring.profiles", "spring.quartz", "spring.reactor", "spring.task",
snippets.add("application-properties.transaction", "Transaction Properties", this::transactionPrefixes); "spring.mandatory-file-encoding", "info", "spring.output.ansi.enabled")
snippets.add("application-properties.data-migration", "Data Migration Properties", this::dataMigrationPrefixes); .addSection("mail").withKeyPrefixes("spring.mail", "spring.sendgrid").addSection("cache")
snippets.add("application-properties.integration", "Integration Properties", this::integrationPrefixes); .withKeyPrefixes("spring.cache").addSection("server").withKeyPrefixes("server").addSection("web")
snippets.add("application-properties.web", "Web Properties", this::webPrefixes); .withKeyPrefixes("spring.hateoas", "spring.http", "spring.servlet", "spring.jersey", "spring.mvc",
snippets.add("application-properties.templating", "Templating Properties", this::templatePrefixes); "spring.resources", "spring.session", "spring.web", "spring.webflux")
snippets.add("application-properties.server", "Server Properties", this::serverPrefixes); .addSection("json").withKeyPrefixes("spring.jackson", "spring.gson").addSection("rsocket")
snippets.add("application-properties.security", "Security Properties", this::securityPrefixes); .withKeyPrefixes("spring.rsocket").addSection("templating")
snippets.add("application-properties.rsocket", "RSocket Properties", this::rsocketPrefixes); .withKeyPrefixes("spring.freemarker", "spring.groovy", "spring.mustache", "spring.thymeleaf")
snippets.add("application-properties.actuator", "Actuator Properties", this::actuatorPrefixes); .addOverride("spring.groovy.template.configuration", "See GroovyMarkupConfigurer")
snippets.add("application-properties.docker-compose", "Docker Compose Properties", this::dockerComposePrefixes); .addSection("security").withKeyPrefixes("spring.security").addSection("data-migration")
snippets.add("application-properties.devtools", "Devtools Properties", this::devtoolsPrefixes); .withKeyPrefixes("spring.flyway", "spring.liquibase").addSection("data")
snippets.add("application-properties.testing", "Testing Properties", this::testingPrefixes); .withKeyPrefixes("spring.couchbase", "spring.elasticsearch", "spring.h2", "spring.influx",
snippets.writeTo(this.outputDir.toPath()); "spring.ldap", "spring.mongodb", "spring.neo4j", "spring.redis", "spring.dao", "spring.data",
} "spring.datasource", "spring.jooq", "spring.jdbc", "spring.jpa", "spring.r2dbc")
.addOverride("spring.datasource.oracleucp",
private void corePrefixes(Config config) { "Oracle UCP specific settings bound to an instance of Oracle UCP's PoolDataSource")
config.accept("debug"); .addOverride("spring.datasource.dbcp2",
config.accept("trace"); "Commons DBCP2 specific settings bound to an instance of DBCP2's BasicDataSource")
config.accept("logging"); .addOverride("spring.datasource.tomcat",
config.accept("spring.aop"); "Tomcat datasource specific settings bound to an instance of Tomcat JDBC's DataSource")
config.accept("spring.application"); .addOverride("spring.datasource.hikari",
config.accept("spring.autoconfigure"); "Hikari specific settings bound to an instance of Hikari's HikariDataSource")
config.accept("spring.banner"); .addSection("transaction").withKeyPrefixes("spring.jta", "spring.transaction").addSection("integration")
config.accept("spring.beaninfo"); .withKeyPrefixes("spring.activemq", "spring.artemis", "spring.batch", "spring.integration",
config.accept("spring.codec"); "spring.jms", "spring.kafka", "spring.rabbitmq", "spring.hazelcast", "spring.webservices")
config.accept("spring.config"); .addSection("actuator").withKeyPrefixes("management").addSection("devtools")
config.accept("spring.info"); .withKeyPrefixes("spring.devtools").addSection("testing").withKeyPrefixes("spring.test");
config.accept("spring.jmx"); DocumentOptions options = builder.build();
config.accept("spring.lifecycle"); new ConfigurationMetadataDocumentWriter().writeDocument(this.outputDir.toPath(), options,
config.accept("spring.main"); this.configurationPropertyMetadata);
config.accept("spring.messages");
config.accept("spring.pid");
config.accept("spring.profiles");
config.accept("spring.quartz");
config.accept("spring.reactor");
config.accept("spring.ssl");
config.accept("spring.task");
config.accept("spring.threads");
config.accept("spring.mandatory-file-encoding");
config.accept("info");
config.accept("spring.output.ansi.enabled");
}
private void cachePrefixes(Config config) {
config.accept("spring.cache");
}
private void mailPrefixes(Config config) {
config.accept("spring.mail");
config.accept("spring.sendgrid");
}
private void jsonPrefixes(Config config) {
config.accept("spring.jackson");
config.accept("spring.gson");
}
private void dataPrefixes(Config config) {
config.accept("spring.couchbase");
config.accept("spring.cassandra");
config.accept("spring.elasticsearch");
config.accept("spring.h2");
config.accept("spring.influx");
config.accept("spring.ldap");
config.accept("spring.mongodb");
config.accept("spring.neo4j");
config.accept("spring.dao");
config.accept("spring.data");
config.accept("spring.datasource");
config.accept("spring.jooq");
config.accept("spring.jdbc");
config.accept("spring.jpa");
config.accept("spring.r2dbc");
config.accept("spring.datasource.oracleucp",
"Oracle UCP specific settings bound to an instance of Oracle UCP's PoolDataSource");
config.accept("spring.datasource.dbcp2",
"Commons DBCP2 specific settings bound to an instance of DBCP2's BasicDataSource");
config.accept("spring.datasource.tomcat",
"Tomcat datasource specific settings bound to an instance of Tomcat JDBC's DataSource");
config.accept("spring.datasource.hikari",
"Hikari specific settings bound to an instance of Hikari's HikariDataSource");
}
private void transactionPrefixes(Config prefix) {
prefix.accept("spring.jta");
prefix.accept("spring.transaction");
}
private void dataMigrationPrefixes(Config prefix) {
prefix.accept("spring.flyway");
prefix.accept("spring.liquibase");
prefix.accept("spring.sql.init");
}
private void integrationPrefixes(Config prefix) {
prefix.accept("spring.activemq");
prefix.accept("spring.artemis");
prefix.accept("spring.batch");
prefix.accept("spring.integration");
prefix.accept("spring.jms");
prefix.accept("spring.kafka");
prefix.accept("spring.pulsar");
prefix.accept("spring.rabbitmq");
prefix.accept("spring.hazelcast");
prefix.accept("spring.webservices");
}
private void webPrefixes(Config prefix) {
prefix.accept("spring.graphql");
prefix.accept("spring.hateoas");
prefix.accept("spring.http");
prefix.accept("spring.jersey");
prefix.accept("spring.mvc");
prefix.accept("spring.netty");
prefix.accept("spring.resources");
prefix.accept("spring.servlet");
prefix.accept("spring.session");
prefix.accept("spring.web");
prefix.accept("spring.webflux");
}
private void templatePrefixes(Config prefix) {
prefix.accept("spring.freemarker");
prefix.accept("spring.groovy");
prefix.accept("spring.mustache");
prefix.accept("spring.thymeleaf");
prefix.accept("spring.groovy.template.configuration", "See GroovyMarkupConfigurer");
}
private void serverPrefixes(Config prefix) {
prefix.accept("server");
}
private void securityPrefixes(Config prefix) {
prefix.accept("spring.security");
}
private void rsocketPrefixes(Config prefix) {
prefix.accept("spring.rsocket");
}
private void actuatorPrefixes(Config prefix) {
prefix.accept("management");
}
private void dockerComposePrefixes(Config prefix) {
prefix.accept("spring.docker.compose");
}
private void devtoolsPrefixes(Config prefix) {
prefix.accept("spring.devtools");
}
private void testingPrefixes(Config prefix) {
prefix.accept("spring.test");
} }
} }

@ -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<String, List<String>> metadataSections;
private final Map<String, String> overrides;
private DocumentOptions(Map<String, List<String>> metadataSections, Map<String, String> overrides) {
this.metadataSections = metadataSections;
this.overrides = overrides;
}
Map<String, List<String>> getMetadataSections() {
return this.metadataSections;
}
Map<String, String> getOverrides() {
return this.overrides;
}
static Builder builder() {
return new Builder();
}
/**
* Builder for DocumentOptions.
*/
public static class Builder {
Map<String, List<String>> metadataSections = new HashMap<>();
Map<String, String> 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;
}
}
}

@ -1,64 +0,0 @@
/*
* Copyright 2012-2021 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 rows in {@link Table}.
*
* @author Brian Clozel
* @author Phillip Webb
*/
abstract class Row implements Comparable<Row> {
private final Snippet snippet;
private final String id;
protected Row(Snippet snippet, String id) {
this.snippet = snippet;
this.id = id;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Row other = (Row) obj;
return this.id.equals(other.id);
}
@Override
public int hashCode() {
return this.id.hashCode();
}
@Override
public int compareTo(Row other) {
return this.id.compareTo(other.id);
}
String getAnchor() {
return this.snippet.getAnchor() + "." + this.id;
}
abstract void write(Asciidoc asciidoc);
}

@ -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.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;
private final String anchor;
SingleConfigurationTableEntry(ConfigurationProperty property) {
this.key = property.getName();
this.anchor = this.key;
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.anchor + "]]<<" + this.anchor + ",`+", 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("|", "\\|").replace("<", "&lt;").replace(">", "&gt;");
builder.append("|+++", cleanedDescription, "+++");
}
}
}

@ -1,85 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.build.context.properties;
import java.util.Arrays;
import java.util.stream.Collectors;
/**
* Table row containing a single configuration property.
*
* @author Brian Clozel
* @author Phillip Webb
*/
class SingleRow extends Row {
private final String displayName;
private final String description;
private final String defaultValue;
SingleRow(Snippet snippet, ConfigurationProperty property) {
super(snippet, property.getName());
this.displayName = property.getDisplayName();
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(Asciidoc asciidoc) {
asciidoc.append("|");
asciidoc.append("[[" + getAnchor() + "]]");
asciidoc.appendln("<<" + getAnchor() + ",`+", this.displayName, "+`>>");
writeDescription(asciidoc);
writeDefaultValue(asciidoc);
}
private void writeDescription(Asciidoc builder) {
if (this.description == null || this.description.isEmpty()) {
builder.appendln("|");
}
else {
String cleanedDescription = this.description.replace("|", "\\|").replace("<", "&lt;").replace(">", "&gt;");
builder.appendln("|+++", cleanedDescription, "+++");
}
}
private void writeDefaultValue(Asciidoc builder) {
String defaultValue = (this.defaultValue != null) ? this.defaultValue : "";
if (defaultValue.isEmpty()) {
builder.appendln("|");
}
else {
defaultValue = defaultValue.replace("\\", "\\\\").replace("|", "\\|");
builder.appendln("|`+", defaultValue, "+`");
}
}
}

@ -1,102 +0,0 @@
/*
* Copyright 2012-2021 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.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
/**
* A configuration properties snippet.
*
* @author Brian Clozed
* @author Phillip Webb
*/
class Snippet {
private final String anchor;
private final String title;
private final Set<String> prefixes;
private final Map<String, String> overrides;
Snippet(String anchor, String title, Consumer<Config> config) {
Set<String> prefixes = new LinkedHashSet<>();
Map<String, String> overrides = new LinkedHashMap<>();
if (config != null) {
config.accept(new Config() {
@Override
public void accept(String prefix) {
prefixes.add(prefix);
}
@Override
public void accept(String prefix, String description) {
overrides.put(prefix, description);
}
});
}
this.anchor = anchor;
this.title = title;
this.prefixes = prefixes;
this.overrides = overrides;
}
String getAnchor() {
return this.anchor;
}
String getTitle() {
return this.title;
}
void forEachPrefix(Consumer<String> action) {
this.prefixes.forEach(action);
}
void forEachOverride(BiConsumer<String, String> action) {
this.overrides.forEach(action);
}
/**
* Callback to configure the snippet.
*/
interface Config {
/**
* Accept the given prefix using the meta-data description.
* @param prefix the prefix to accept
*/
void accept(String prefix);
/**
* Accept the given prefix with a defined description.
* @param prefix the prefix to accept
* @param description the description to use
*/
void accept(String prefix, String description);
}
}

@ -1,133 +0,0 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.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.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.gradle.api.file.FileCollection;
/**
* Configuration properties snippets.
*
* @author Brian Clozed
* @author Phillip Webb
*/
class Snippets {
private final ConfigurationProperties properties;
private final List<Snippet> snippets = new ArrayList<>();
Snippets(FileCollection configurationPropertyMetadata) {
this.properties = ConfigurationProperties.fromFiles(configurationPropertyMetadata);
}
void add(String anchor, String title, Consumer<Snippet.Config> config) {
this.snippets.add(new Snippet(anchor, title, config));
}
void writeTo(Path outputDirectory) throws IOException {
createDirectory(outputDirectory);
Set<String> remaining = this.properties.stream()
.filter((property) -> !property.isDeprecated())
.map(ConfigurationProperty::getName)
.collect(Collectors.toSet());
for (Snippet snippet : this.snippets) {
Set<String> written = writeSnippet(outputDirectory, snippet, remaining);
remaining.removeAll(written);
}
if (!remaining.isEmpty()) {
throw new IllegalStateException(
"The following keys were not written to the documentation: " + String.join(", ", remaining));
}
}
private Set<String> writeSnippet(Path outputDirectory, Snippet snippet, Set<String> remaining) throws IOException {
Table table = new Table();
Set<String> added = new HashSet<>();
snippet.forEachOverride((prefix, description) -> {
CompoundRow row = new CompoundRow(snippet, prefix, description);
remaining.stream().filter((candidate) -> candidate.startsWith(prefix)).forEach((name) -> {
if (added.add(name)) {
row.addProperty(this.properties.get(name));
}
});
table.addRow(row);
});
snippet.forEachPrefix((prefix) -> {
remaining.stream().filter((candidate) -> candidate.startsWith(prefix)).forEach((name) -> {
if (added.add(name)) {
table.addRow(new SingleRow(snippet, this.properties.get(name)));
}
});
});
Asciidoc asciidoc = getAsciidoc(snippet, table);
writeAsciidoc(outputDirectory, snippet, asciidoc);
return added;
}
private Asciidoc getAsciidoc(Snippet snippet, Table table) {
Asciidoc asciidoc = new Asciidoc();
// We have to prepend 'appendix.' as a section id here, otherwise the
// spring-asciidoctor-extensions:section-id asciidoctor extension complains
asciidoc.appendln("[[appendix." + snippet.getAnchor() + "]]");
asciidoc.appendln("== ", snippet.getTitle());
table.write(asciidoc);
return asciidoc;
}
private void writeAsciidoc(Path outputDirectory, Snippet snippet, Asciidoc asciidoc) throws IOException {
String[] parts = (snippet.getAnchor()).split("\\.");
Path path = outputDirectory;
for (int i = 0; i < parts.length; i++) {
String name = (i < parts.length - 1) ? parts[i] : parts[i] + ".adoc";
path = path.resolve(name);
}
createDirectory(path.getParent());
Files.deleteIfExists(path);
try (OutputStream outputStream = Files.newOutputStream(path)) {
outputStream.write(asciidoc.toString().getBytes(StandardCharsets.UTF_8));
}
}
private void createDirectory(Path path) throws IOException {
assertValidOutputDirectory(path);
if (!Files.exists(path)) {
Files.createDirectory(path);
}
}
private void assertValidOutputDirectory(Path path) {
if (path == null) {
throw new IllegalArgumentException("Directory path should not be null");
}
if (Files.exists(path) && !Files.isDirectory(path)) {
throw new IllegalArgumentException("Path already exists and is not a directory");
}
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save