diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index a59731f4ec93..eac073d7e630 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -3,7 +3,7 @@ on: workflow_dispatch: push: branches: - - '4.0.x' + - 'main' permissions: contents: read concurrency: @@ -30,7 +30,7 @@ jobs: - name: Deploy uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4 with: - build-name: ${{ vars.COMMERCIAL && format('spring-boot-commercial-{0}', '4.0.x') || format('spring-boot-{0}', '4.0.x') }} + build-name: ${{ vars.COMMERCIAL && format('spring-boot-commercial-{0}', '4.2.x') || format('spring-boot-{0}', '4.2.x') }} folder: 'deployment-repository' password: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_PASSWORD || secrets.ARTIFACTORY_PASSWORD }} project: ${{ vars.COMMERCIAL && 'spring' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0905e0790f7a..5cff4b0e7a22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: branches: - - '4.0.x' + - 'main' permissions: contents: read jobs: diff --git a/.github/workflows/release-milestone.yml b/.github/workflows/release-milestone.yml index 03cfbf778e4f..c51321e9c704 100644 --- a/.github/workflows/release-milestone.yml +++ b/.github/workflows/release-milestone.yml @@ -2,8 +2,8 @@ name: Release Milestone on: push: tags: - - v4.0.0-M[0-9] - - v4.0.0-RC[0-9] + - v4.2.0-M[0-9] + - v4.2.0-RC[0-9] permissions: contents: read concurrency: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d4288c5d85ab..7eb72877a5cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release on: push: tags: - - v4.0.[0-9]+ + - v4.2.[0-9]+ permissions: contents: read concurrency: @@ -126,7 +126,7 @@ jobs: - name: Publish to SDKMAN! uses: ./.github/actions/publish-to-sdkman with: - make-default: false + make-default: true sdkman-consumer-key: ${{ secrets.SDKMAN_CONSUMER_KEY }} sdkman-consumer-token: ${{ secrets.SDKMAN_CONSUMER_TOKEN }} spring-boot-version: ${{ needs.build-and-stage-release.outputs.version }} @@ -174,5 +174,6 @@ jobs: uses: ./.github/actions/create-github-release with: commercial: ${{ vars.COMMERCIAL }} + latest: true milestone: ${{ needs.build-and-stage-release.outputs.version }} token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/.github/workflows/run-system-tests.yml b/.github/workflows/run-system-tests.yml index 340080d3c734..76513b260e89 100644 --- a/.github/workflows/run-system-tests.yml +++ b/.github/workflows/run-system-tests.yml @@ -2,12 +2,12 @@ name: Run System Tests on: push: branches: - - '4.0.x' + - "main" permissions: contents: read jobs: run-system-tests: - name: 'Java ${{ matrix.java.version}}' + name: "Java ${{ matrix.java.version}}" if: ${{ github.repository == 'spring-projects/spring-boot' }} runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }} strategy: diff --git a/.github/workflows/trigger-docs-build.yml b/.github/workflows/trigger-docs-build.yml index 600c97f834a9..da70502b6914 100644 --- a/.github/workflows/trigger-docs-build.yml +++ b/.github/workflows/trigger-docs-build.yml @@ -1,7 +1,7 @@ name: Trigger Docs Build on: push: - branches: '4.0.x' + branches: 'main' paths: [ 'antora/*' ] workflow_dispatch: inputs: diff --git a/README.adoc b/README.adoc index 65c70c823d1b..7eb4dda13491 100755 --- a/README.adoc +++ b/README.adoc @@ -1,4 +1,4 @@ -= Spring Boot image:https://github.com/spring-projects/spring-boot/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=4.0.x["Build Status", link="https://github.com/spring-projects/spring-boot/actions/workflows/build-and-deploy-snapshot.yml?query=branch%3A4.0.x"] image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?&search.rootProjectNames=Spring%20Boot%20Build&search.rootProjectNames=spring-boot-build"] += Spring Boot image:https://github.com/spring-projects/spring-boot/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=main["Build Status", link="https://github.com/spring-projects/spring-boot/actions/workflows/build-and-deploy-snapshot.yml?query=branch%3Amain"] image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A["Revved up by Develocity", link="https://ge.spring.io/scans?&search.rootProjectNames=Spring%20Boot%20Build&search.rootProjectNames=spring-boot-build"] :docs: https://docs.spring.io/spring-boot :github: https://github.com/spring-projects/spring-boot diff --git a/build-plugin/spring-boot-gradle-plugin/build.gradle b/build-plugin/spring-boot-gradle-plugin/build.gradle index eaa81a3aac98..a9e0f3918713 100644 --- a/build-plugin/spring-boot-gradle-plugin/build.gradle +++ b/build-plugin/spring-boot-gradle-plugin/build.gradle @@ -49,6 +49,7 @@ dependencies { implementation("org.apache.commons:commons-compress") implementation("org.springframework:spring-core") + optional("com.google.protobuf:protobuf-gradle-plugin") optional("org.graalvm.buildtools:native-gradle-plugin") optional("org.cyclonedx:cyclonedx-gradle-plugin") { exclude(group: "javax.annotation", module: "javax.annotation-api") @@ -64,6 +65,7 @@ dependencies { testImplementation("org.apache.httpcomponents.client5:httpclient5") testImplementation("org.graalvm.buildtools:native-gradle-plugin") testImplementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + testImplementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:$kotlinVersion") testImplementation("org.jetbrains.kotlin:kotlin-compiler-runner:$kotlinVersion") testImplementation("org.jetbrains.kotlin:kotlin-daemon-client:$kotlinVersion") testImplementation("org.tomlj:tomlj:1.0.0") diff --git a/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java index 62035296d245..503c2463eb23 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/dockerTest/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageIntegrationTests.java @@ -469,6 +469,20 @@ void buildsImageOnLinuxArmWithImagePlatformLinuxArm() throws IOException { removeImages(projectName, builderImage, runImage, buildpackImage); } + @TestTemplate + void buildsImageWithMultipleCommandLineEnvironments() throws IOException { + writeMainClass(); + writeLongNameResource(); + BuildResult result = this.gradleBuild.build("bootBuildImage", "--environment", "BP_LIVE_RELOAD_ENABLED=true", + "--environment", "MY_CUSTOM_VAR=hello_world"); + BuildTask task = result.task(":bootBuildImage"); + assertThat(task).isNotNull(); + assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("BP_LIVE_RELOAD_ENABLED=true"); + assertThat(result.getOutput()).contains("MY_CUSTOM_VAR=hello_world"); + removeImages(this.gradleBuild.getProjectDir().getName()); + } + @TestTemplate @EnabledOnOs(value = { OS.LINUX, OS.MAC }, architectures = "amd64", disabledReason = "The expected failure condition will not fail on ARM architectures") diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle new file mode 100644 index 000000000000..14ffe5e14446 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle @@ -0,0 +1,24 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version-spring-boot}' +} + +tasks.named("bootJar") { + mainClass = 'com.example.ExampleApplication' +} + +// tag::caches[] +tasks.named("bootBuildImage") { + buildCache { + image { + name = "docker.io/library/${rootProject.name}:build" + } + } +} +// end::caches[] + +tasks.register("bootBuildImageCaches") { + doFirst { + bootBuildImage.buildCache.asCache().with { println "buildCache=$name" } + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts new file mode 100644 index 000000000000..ea20581a168f --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/examples/packaging/boot-build-image-image-caches.gradle.kts @@ -0,0 +1,22 @@ +import org.springframework.boot.gradle.tasks.bundling.BootBuildImage + +plugins { + java + id("org.springframework.boot") version "{version-spring-boot}" +} + +// tag::caches[] +tasks.named("bootBuildImage") { + buildCache { + image { + name.set("docker.io/library/${rootProject.name}:build") + } + } +} +// end::caches[] + +tasks.register("bootBuildImageCaches") { + doFirst { + println("buildCache=" + tasks.getByName("bootBuildImage").buildCache.asCache()?.image?.name) + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/integrating-with-actuator.adoc b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/integrating-with-actuator.adoc index 4cf5ea38402d..c3f4dbfb7205 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/integrating-with-actuator.adoc +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/integrating-with-actuator.adoc @@ -26,8 +26,9 @@ include::example$integrating-with-actuator/build-info-basic.gradle.kts[tags=buil ---- ====== -This will configure a {apiref-gradle-plugin-boot-build-info}[`BuildInfo`] task named `bootBuildInfo` and, if it exists, make the Java plugin's `classes` task depend upon it. -The task's destination directory will be `META-INF` in the output directory of the main source set's resources (typically `build/resources/main`). +This will configure a {apiref-gradle-plugin-boot-build-info}[`BuildInfo`] task named `bootBuildInfo`. +The task's destination directory will be `build/bootBuildInfo` and its filename will be `META-INF/build-info.properties`. +If the main source set exists, its resources are configured to use the output of the `bootBuildInfo` task as a src dir. By default, the generated build information is derived from the project: diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc index 139eeda2adb0..9ffcbe13a296 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/packaging-oci-image.adoc @@ -162,7 +162,7 @@ Acceptable values are `ALWAYS`, `NEVER`, and `IF_NOT_PRESENT`. | `ALWAYS` | `environment` -| +| `--environment` | Environment variables that should be passed to the builder. | Empty. @@ -346,6 +346,15 @@ include::example$packaging/boot-build-image-env.gradle.kts[tags=env] ---- ====== +Environment variables can also be specified on the command line, as shown in the following example: + +[source,shell] +---- +$ gradle bootBuildImage --environment BP_JVM_VERSION=17 +---- + +`--environment` can be used multiple times to specify multiple environment variables. + If there is a network proxy between the Docker daemon the builder runs in and network locations that buildpacks download artifacts from, you will need to configure the builder to use the proxy. When using the Paketo builder, this can be accomplished by setting the `HTTPS_PROXY` and/or `HTTP_PROXY` environment variables as show in the following example: @@ -564,6 +573,24 @@ include::example$packaging/boot-build-image-bind-caches.gradle.kts[tags=caches] ---- ====== +The build cache can also be stored in an image instead of a named volume, which allows the cache to be shared between hosts by pushing and pulling it from an image registry, as shown in the following example: + +[tabs] +====== +Groovy:: ++ +[source,groovy,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging/boot-build-image-image-caches.gradle[tags=caches] +---- +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging/boot-build-image-image-caches.gradle.kts[tags=caches] +---- +====== + [[build-image.examples.docker]] diff --git a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc index d1ec0b5817b5..33558dd5a706 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc +++ b/build-plugin/spring-boot-gradle-plugin/src/docs/antora/modules/gradle-plugin/pages/reacting.adoc @@ -104,3 +104,17 @@ When the {url-cyclonedx-docs-gradle-plugin}[CycloneDX plugin] is applied to a pr .. Disable full license texts. . Adds the SBOM under `META-INF/sbom` in the generated jar or war file. . Adds the `Sbom-Format` and `Sbom-Location` to the manifest of the jar or war file. + + + +[[reacting-to-other-plugins.protobuf]] +== Reacting to the Protobuf Plugin + +When the {url-protobuf-docs-gradle-plugin}[Protobuf plugin] is applied to a project, the Spring Boot plugin: + +. Configures `protoc` to use the artifact `com.google.protobuf:protoc`, aligning its version with that of Protobuf dependencies on the runtime classpath. + +Additionally, when the Protobuf `grpc` plugin is configured, the Spring Boot plugin: + +. Configures it to use the artifact `io.grpc:protoc-gen-grpc-java`, aligning its version with that of gRPC dependencies on the runtime classpath. +. Configures the `grpc` plugin of all generate proto tasks with the option `@generated=omit`. diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java index e6f42863de59..67b6940dd454 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/dsl/SpringBootExtension.java @@ -16,10 +16,9 @@ package org.springframework.boot.gradle.dsl; -import java.io.File; - import org.gradle.api.Action; import org.gradle.api.Project; +import org.gradle.api.file.SourceDirectorySet; import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; @@ -31,7 +30,6 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.gradle.tasks.buildinfo.BuildInfo; -import org.springframework.util.Assert; /** * Entry point to Spring Boot's Gradle DSL. @@ -66,25 +64,25 @@ public Property getMainClass() { } /** - * Creates a new {@link BuildInfo} task named {@code bootBuildInfo} and configures the - * Java plugin's {@code classes} task to depend upon it. + * Creates a new {@link BuildInfo} task named {@code bootBuildInfo} and adds its + * output as a {@link SourceDirectorySet#srcDir source directory} of the main source + * set's {@link SourceSet#getResources resources}. *

- * By default, the task's destination dir will be a directory named {@code META-INF} - * beneath the main source set's resources output directory, and the task's project - * artifact will be the base name of the {@code bootWar} or {@code bootJar} task. + * By default, the task's project artifact will be the archive base name of the + * {@code bootWar} or {@code bootJar} task. */ public void buildInfo() { buildInfo(null); } /** - * Creates a new {@link BuildInfo} task named {@code bootBuildInfo} and configures the - * Java plugin's {@code classes} task to depend upon it. The task is passed to the - * given {@code configurer} for further configuration. + * Creates a new {@link BuildInfo} task named {@code bootBuildInfo} and adds its + * output as a {@link SourceDirectorySet#srcDir source directory} of the main source + * set's {@link SourceSet#getResources resources}. The task is passed to the given + * {@code configurer} for further configuration. *

- * By default, the task's destination dir will be a directory named {@code META-INF} - * beneath the main source set's resources output directory, and the task's project - * artifact will be the base name of the {@code bootWar} or {@code bootJar} task. + * By default, the task's project artifact will be the archive base name of the + * {@code bootWar} or {@code bootJar} task. * @param configurer the task configurer */ public void buildInfo(@Nullable Action configurer) { @@ -92,10 +90,15 @@ public void buildInfo(@Nullable Action configurer) { TaskProvider bootBuildInfo = tasks.register("bootBuildInfo", BuildInfo.class, this::configureBuildInfoTask); this.project.getPlugins().withType(JavaPlugin.class, (plugin) -> { - tasks.named(JavaPlugin.CLASSES_TASK_NAME).configure((task) -> task.dependsOn(bootBuildInfo)); bootBuildInfo.configure((buildInfo) -> buildInfo.getProperties() .getArtifact() .convention(this.project.provider(this::determineArtifactBaseName))); + this.project.getExtensions() + .getByType(JavaPluginExtension.class) + .getSourceSets() + .getByName(SourceSet.MAIN_SOURCE_SET_NAME) + .getResources() + .srcDir(bootBuildInfo); }); if (configurer != null) { bootBuildInfo.configure(configurer); @@ -105,20 +108,6 @@ public void buildInfo(@Nullable Action configurer) { private void configureBuildInfoTask(BuildInfo task) { task.setGroup(BasePlugin.BUILD_GROUP); task.setDescription("Generates a META-INF/build-info.properties file."); - task.getDestinationDir() - .convention(this.project.getLayout() - .dir(this.project.provider(() -> new File(determineMainSourceSetResourcesOutputDir(), "META-INF")))); - } - - private File determineMainSourceSetResourcesOutputDir() { - File resourcesDir = this.project.getExtensions() - .getByType(JavaPluginExtension.class) - .getSourceSets() - .getByName(SourceSet.MAIN_SOURCE_SET_NAME) - .getOutput() - .getResourcesDir(); - Assert.state(resourcesDir != null, "'resourcesDir' must not be null"); - return resourcesDir; } private @Nullable String determineArtifactBaseName() { diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java new file mode 100644 index 000000000000..338bd21b451d --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ProtobufPluginAction.java @@ -0,0 +1,158 @@ +/* + * Copyright 2012-present 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.gradle.plugin; + +import java.util.List; +import java.util.Optional; + +import com.google.protobuf.gradle.ExecutableLocator; +import com.google.protobuf.gradle.GenerateProtoTask; +import com.google.protobuf.gradle.GenerateProtoTask.PluginOptions; +import com.google.protobuf.gradle.ProtobufExtension; +import com.google.protobuf.gradle.ProtobufExtension.GenerateProtoTaskCollection; +import com.google.protobuf.gradle.ProtobufPlugin; +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencyResolveDetails; +import org.gradle.api.artifacts.ModuleVersionSelector; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; +import org.gradle.api.artifacts.result.ResolvedComponentResult; +import org.jspecify.annotations.Nullable; + +/** + * {@link Action} that is executed in response to the {@link ProtobufPlugin} being + * applied. + * + * @author Andy Wilkinson + * @author Dongliang Xie + */ +final class ProtobufPluginAction implements PluginApplicationAction { + + private static final Dependency protocDependency = new Dependency("com.google.protobuf", "protoc"); + + private static final Dependency grpcDependency = new Dependency("io.grpc", "protoc-gen-grpc-java"); + + private static final List versionAlignment = List.of( + protocDependency.alignVersionWith("com.google.protobuf", "protobuf-java"), + grpcDependency.alignVersionWith("io.grpc", "grpc-util")); + + @Override + public Class> getPluginClass() { + return ProtobufPlugin.class; + } + + @Override + public void execute(Project project) { + ProtobufExtension protobuf = project.getExtensions().getByType(ProtobufExtension.class); + protobuf.protoc(this::configureProtoc); + protobuf.plugins((plugins) -> configurePlugins(plugins, protobuf)); + project.getConfigurations() + .named(this::isProtobufToolsLocator) + .configureEach((configuration) -> configureProtobufToolsLocator(project, configuration)); + } + + private void configureProtoc(ExecutableLocator protoc) { + protoc.setArtifact(protocDependency.asDependencySpec()); + } + + private void configurePlugins(NamedDomainObjectContainer plugins, ProtobufExtension protobuf) { + plugins.matching((plugin) -> "grpc".equals(plugin.getName())).configureEach((grpc) -> { + grpc.setArtifact(grpcDependency.asDependencySpec()); + protobuf.generateProtoTasks(this::configureGenerateProtoTasks); + }); + } + + private void configureGenerateProtoTasks(GenerateProtoTaskCollection tasks) { + tasks.all().configureEach(this::configureGenerateProtoTask); + } + + private void configureGenerateProtoTask(GenerateProtoTask task) { + task.plugins((plugins) -> plugins.create("grpc", this::configureGrpcOptions)); + } + + private void configureGrpcOptions(PluginOptions grpc) { + grpc.option("@generated=omit"); + } + + private boolean isProtobufToolsLocator(String name) { + return name.startsWith("protobufToolsLocator_"); + } + + private void configureProtobufToolsLocator(Project project, Configuration configuration) { + configuration.getResolutionStrategy().eachDependency((details) -> { + VersionAlignment versionAlignment = versionAlignmentFor(details); + if (versionAlignment != null) { + versionAlignment.applyIfPossible(project, details); + } + }); + } + + private @Nullable VersionAlignment versionAlignmentFor(DependencyResolveDetails details) { + if ("null".equals(details.getRequested().getVersion())) { + for (VersionAlignment alignment : versionAlignment) { + if (alignment.accepts(details)) { + return alignment; + } + } + } + return null; + } + + private record Dependency(String group, String module) { + + private VersionAlignment alignVersionWith(String group, String module) { + return new VersionAlignment(this, new Dependency(group, module)); + } + + private String asDependencySpec() { + return this.group + ":" + this.module; + } + + } + + private record VersionAlignment(Dependency target, Dependency source) { + + void applyIfPossible(Project project, DependencyResolveDetails details) { + versionFromRuntimeClasspath(project, source()).ifPresent(details::useVersion); + } + + boolean accepts(DependencyResolveDetails details) { + ModuleVersionSelector requested = details.getRequested(); + return target().group().equals(requested.getGroup()) && target().module().equals(requested.getName()); + } + + private Optional versionFromRuntimeClasspath(Project project, Dependency source) { + return project.getConfigurations() + .getByName("runtimeClasspath") + .getIncoming() + .getResolutionResult() + .getAllComponents() + .stream() + .map(ResolvedComponentResult::getId) + .filter(ModuleComponentIdentifier.class::isInstance) + .map(ModuleComponentIdentifier.class::cast) + .filter((id) -> id.getGroup().equals(source.group()) && id.getModule().equals(source.module())) + .map(ModuleComponentIdentifier::getVersion) + .findFirst(); + } + + } + +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java index 9c9d9e026c31..48b07bc2fff0 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/SpringBootPlugin.java @@ -148,7 +148,7 @@ private void registerPluginActions(Project project, Configuration bootArchives) List actions = Arrays.asList(new JavaPluginAction(singlePublishedArtifact), new WarPluginAction(singlePublishedArtifact), new DependencyManagementPluginAction(), new ApplicationPluginAction(), new KotlinPluginAction(), new NativeImagePluginAction(), - new CyclonedxPluginAction()); + new CyclonedxPluginAction(), new ProtobufPluginAction()); for (PluginApplicationAction action : actions) { withPluginClassOfAction(action, (pluginClass) -> project.getPlugins().withType(pluginClass, (plugin) -> action.execute(project))); diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java index d09b99d1bf9e..07742f197882 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfo.java @@ -23,7 +23,9 @@ import org.gradle.api.DefaultTask; import org.gradle.api.Task; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.provider.Property; import org.gradle.api.provider.SetProperty; +import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.OutputDirectory; @@ -35,8 +37,8 @@ import org.springframework.boot.loader.tools.BuildPropertiesWriter.ProjectDetails; /** - * {@link Task} for generating a {@code build-info.properties} file from a - * {@code Project}. + * {@link Task} for generating a build info properties file from + * {@link BuildInfoProperties}. * * @author Andy Wilkinson * @since 2.0.0 @@ -49,6 +51,7 @@ public abstract class BuildInfo extends DefaultTask { public BuildInfo() { this.properties = getProject().getObjects().newInstance(BuildInfoProperties.class, getExcludes()); getDestinationDir().convention(getProject().getLayout().getBuildDirectory().dir(getName())); + getFilename().convention("META-INF/build-info.properties"); } /** @@ -60,8 +63,17 @@ public BuildInfo() { public abstract SetProperty getExcludes(); /** - * Generates the {@code build-info.properties} file in the configured - * {@link #getDestinationDir destination}. + * Returns the name of the file that is written to the {@link #getDestinationDir + * destination dir}. Convention is {@code META-INF/build-info.properties}. + * @return the name of the written file + * @since 4.1.0 + */ + @Input + public abstract Property getFilename(); + + /** + * Generates the build info properties file in the configured + * {@link #getDestinationDir destination dir}. */ @TaskAction public void generateBuildProperties() { @@ -70,8 +82,8 @@ public void generateBuildProperties() { this.properties.getArtifactIfNotExcluded(), this.properties.getVersionIfNotExcluded(), this.properties.getNameIfNotExcluded(), this.properties.getTimeIfNotExcluded(), this.properties.getAdditionalIfNotExcluded()); - new BuildPropertiesWriter(new File(getDestinationDir().get().getAsFile(), "build-info.properties")) - .writeBuildProperties(details); + File outputFile = new File(getDestinationDir().getAsFile().get(), getFilename().get()); + new BuildPropertiesWriter(outputFile).writeBuildProperties(details); } catch (IOException ex) { throw new TaskExecutionException(this, ex); @@ -79,8 +91,8 @@ public void generateBuildProperties() { } /** - * Returns the directory to which the {@code build-info.properties} file will be - * written. + * Returns the directory to which the build info file will be written. Convention is + * build/${taskName}. * @return the destination directory */ @OutputDirectory @@ -88,7 +100,7 @@ public void generateBuildProperties() { /** * Returns the {@link BuildInfoProperties properties} that will be included in the - * {@code build-info.properties} file. + * written file. * @return the properties */ @Nested diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java index 4029f248f0d7..c9ba61878404 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoProperties.java @@ -38,7 +38,7 @@ import org.jspecify.annotations.Nullable; /** - * The properties that are written into the {@code build-info.properties} file. + * The properties that are written into the {@code META-INF/build-info.properties} file. * * @author Andy Wilkinson * @since 2.0.0 diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java index 1945c156bd6d..09939ea281a7 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImage.java @@ -17,11 +17,14 @@ package org.springframework.boot.gradle.tasks.bundling; import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.gradle.api.Action; import org.gradle.api.DefaultTask; +import org.gradle.api.InvalidUserDataException; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.file.RegularFileProperty; @@ -30,6 +33,7 @@ import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.Nested; import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.PathSensitive; @@ -43,6 +47,7 @@ import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.Cache; import org.springframework.boot.buildpack.platform.build.Creator; +import org.springframework.boot.buildpack.platform.build.LocalCache; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.transport.DockerEngineException; import org.springframework.boot.buildpack.platform.docker.type.Binding; @@ -71,11 +76,11 @@ public abstract class BootBuildImage extends DefaultTask { private final String projectName; - private final CacheSpec buildWorkspace; + private final LocalCacheSpec buildWorkspace; private final CacheSpec buildCache; - private final CacheSpec launchCache; + private final LocalCacheSpec launchCache; private final DockerSpec docker; @@ -96,12 +101,29 @@ public BootBuildImage() { getCleanCache().convention(false); getVerboseLogging().convention(false); getPublish().convention(false); - this.buildWorkspace = getProject().getObjects().newInstance(CacheSpec.class); + this.buildWorkspace = getProject().getObjects().newInstance(LocalCacheSpec.class); this.buildCache = getProject().getObjects().newInstance(CacheSpec.class); - this.launchCache = getProject().getObjects().newInstance(CacheSpec.class); + this.launchCache = getProject().getObjects().newInstance(LocalCacheSpec.class); this.docker = getProject().getObjects().newInstance(DockerSpec.class); this.pullPolicy = getProject().getObjects().property(PullPolicy.class); getSecurityOptions().convention((Iterable) null); + getEffectiveEnvironment().putAll(getEnvironment()); + getEffectiveEnvironment().putAll(getEnvironmentFromCommandLine().map(BootBuildImage::asMap)); + } + + private static Map asMap(List variables) { + Map environment = new LinkedHashMap<>(); + for (String variable : variables) { + int index = variable.indexOf('='); + if (index <= 0) { + throw new InvalidUserDataException( + "Invalid value for option '--environment'. Expected 'NAME=VALUE' but got '" + variable + "'."); + } + String name = variable.substring(0, index); + String value = variable.substring(index + 1); + environment.put(name, value); + } + return environment; } /** @@ -157,9 +179,22 @@ public BootBuildImage() { * Returns the environment that will be used when building the image. * @return the environment */ - @Input + @Internal public abstract MapProperty getEnvironment(); + /** + * Returns environment variables contributed from the command line. Each entry must be + * in the form NAME=VALUE. + * @return the environment variables from the command line + */ + @Internal + @Option(option = "environment", description = "Environment variable that will be used when building the image " + + "(NAME=VALUE). Can be specified multiple times.") + abstract ListProperty getEnvironmentFromCommandLine(); + + @Input + abstract MapProperty getEffectiveEnvironment(); + /** * Returns whether caches should be cleaned before packaging. * @return whether caches should be cleaned @@ -246,17 +281,17 @@ public void setPullPolicy(String pullPolicy) { */ @Nested @Optional - public CacheSpec getBuildWorkspace() { + public LocalCacheSpec getBuildWorkspace() { return this.buildWorkspace; } /** - * Customizes the {@link CacheSpec} for the build temporary workspace using the given - * {@code action}. + * Customizes the {@link LocalCacheSpec} for the build temporary workspace using the + * given {@code action}. * @param action the action * @since 3.2.0 */ - public void buildWorkspace(Action action) { + public void buildWorkspace(Action action) { action.execute(this.buildWorkspace); } @@ -285,16 +320,16 @@ public void buildCache(Action action) { */ @Nested @Optional - public CacheSpec getLaunchCache() { + public LocalCacheSpec getLaunchCache() { return this.launchCache; } /** - * Customizes the {@link CacheSpec} for the launch cache using the given + * Customizes the {@link LocalCacheSpec} for the launch cache using the given * {@code action}. * @param action the action */ - public void launchCache(Action action) { + public void launchCache(Action action) { action.execute(this.launchCache); } @@ -413,8 +448,8 @@ private BuildRequest customizeRunImage(BuildRequest request) { } private BuildRequest customizeEnvironment(BuildRequest request) { - Map environment = getEnvironment().getOrNull(); - if (!CollectionUtils.isEmpty(environment)) { + Map environment = getEffectiveEnvironment().getOrElse(Collections.emptyMap()); + if (!environment.isEmpty()) { request = request.withEnv(environment); } return request; @@ -461,7 +496,7 @@ private BuildRequest customizeTags(BuildRequest request) { } private BuildRequest customizeCaches(BuildRequest request) { - Cache buildWorkspaceCache = this.buildWorkspace.asCache(); + LocalCache buildWorkspaceCache = this.buildWorkspace.asCache(); if (buildWorkspaceCache != null) { request = request.withBuildWorkspace(buildWorkspaceCache); } @@ -469,7 +504,7 @@ private BuildRequest customizeCaches(BuildRequest request) { if (buildCache != null) { request = request.withBuildCache(buildCache); } - Cache launchCache = this.launchCache.asCache(); + LocalCache launchCache = this.launchCache.asCache(); if (launchCache != null) { request = request.withLaunchCache(launchCache); } diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java index b459b0cb205b..303b1b63556c 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/CacheSpec.java @@ -26,6 +26,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.gradle.tasks.bundling.LocalCacheSpec.BindCacheSpec; +import org.springframework.boot.gradle.tasks.bundling.LocalCacheSpec.VolumeCacheSpec; /** * Configuration for an image building cache. @@ -75,30 +77,30 @@ public void bind(Action action) { } /** - * Configuration for an image building cache stored in a Docker volume. + * Configures an image cache using the given {@code action}. + * @param action the action + * @since 4.2.0 */ - public abstract static class VolumeCacheSpec { - - /** - * Returns the name of the cache. - * @return the cache name - */ - @Input - public abstract Property getName(); - + public void image(Action action) { + if (this.cache != null) { + throw new GradleException("Each image building cache can be configured only once"); + } + ImageCacheSpec spec = this.objectFactory.newInstance(ImageCacheSpec.class); + action.execute(spec); + this.cache = Cache.image(spec.getName().get()); } /** - * Configuration for an image building cache stored in a bind mount. + * Configuration for an image building cache stored in an image. */ - public abstract static class BindCacheSpec { + public abstract static class ImageCacheSpec { /** - * Returns the source of the cache. - * @return the cache source + * Returns the name of the cache image. + * @return the cache image name */ @Input - public abstract Property getSource(); + public abstract Property getName(); } diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LocalCacheSpec.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LocalCacheSpec.java new file mode 100644 index 000000000000..0283f77546e7 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LocalCacheSpec.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-present 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.gradle.tasks.bundling; + +import javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.GradleException; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.buildpack.platform.build.LocalCache; + +/** + * Configuration for a local image building cache stored in a volume or bind mount. + * + * @author Scott Frederick + * @author Stephane Nicoll + * @since 4.2.0 + */ +public class LocalCacheSpec { + + private final ObjectFactory objectFactory; + + private @Nullable LocalCache cache; + + @Inject + public LocalCacheSpec(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + public @Nullable LocalCache asCache() { + return this.cache; + } + + /** + * Configures a volume cache using the given {@code action}. + * @param action the action + */ + public void volume(Action action) { + if (this.cache != null) { + throw new GradleException("Each image building cache can be configured only once"); + } + VolumeCacheSpec spec = this.objectFactory.newInstance(VolumeCacheSpec.class); + action.execute(spec); + this.cache = Cache.volume(spec.getName().get()); + } + + /** + * Configures a bind cache using the given {@code action}. + * @param action the action + */ + public void bind(Action action) { + if (this.cache != null) { + throw new GradleException("Each image building cache can be configured only once"); + } + BindCacheSpec spec = this.objectFactory.newInstance(BindCacheSpec.class); + action.execute(spec); + this.cache = Cache.bind(spec.getSource().get()); + } + + /** + * Configuration for an image building cache stored in a Docker volume. + */ + public abstract static class VolumeCacheSpec { + + /** + * Returns the name of the cache. + * @return the cache name + */ + @Input + public abstract Property getName(); + + } + + /** + * Configuration for an image building cache stored in a bind mount. + */ + public abstract static class BindCacheSpec { + + /** + * Returns the source of the cache. + * @return the cache source + */ + @Input + public abstract Property getSource(); + + } + +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/AotDocumentationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/AotDocumentationTests.java index 54beb9af816f..8ebdf513712e 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/AotDocumentationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/AotDocumentationTests.java @@ -37,9 +37,11 @@ class AotDocumentationTests { @TestTemplate void applyNativeImagePlugin() { - assertThat(this.gradleBuild.script(Examples.DIR + "aot/apply-native-image-plugin").build("tasks").getOutput()) - .contains("nativeCompile") - .contains("aotClasses"); + assertThat(this.gradleBuild.script(Examples.DIR + "aot/apply-native-image-plugin") + .expectDeprecationMessages("nativeBuild - Deprecated alias for nativeCompile.", + "nativeTestBuild - Deprecated alias for nativeTestCompile.") + .build("tasks") + .getOutput()).contains("nativeCompile").contains("aotClasses"); } @TestTemplate diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java index d09620860135..a5d39037f7b9 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/IntegratingWithActuatorDocumentationTests.java @@ -44,7 +44,7 @@ class IntegratingWithActuatorDocumentationTests { @TestTemplate void basicBuildInfo() { this.gradleBuild.script(Examples.DIR + "integrating-with-actuator/build-info-basic").build("bootBuildInfo"); - assertThat(new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties")) + assertThat(new File(this.gradleBuild.getProjectDir(), "build/bootBuildInfo/META-INF/build-info.properties")) .isFile(); } @@ -52,7 +52,7 @@ void basicBuildInfo() { void buildInfoCustomValues() { this.gradleBuild.script(Examples.DIR + "integrating-with-actuator/build-info-custom-values") .build("bootBuildInfo"); - File file = new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties"); + File file = new File(this.gradleBuild.getProjectDir(), "build/bootBuildInfo/META-INF/build-info.properties"); assertThat(file).isFile(); Properties properties = buildInfoProperties(file); assertThat(properties).containsEntry("build.artifact", "example-app"); @@ -66,7 +66,7 @@ void buildInfoCustomValues() { void buildInfoAdditional() { this.gradleBuild.script(Examples.DIR + "integrating-with-actuator/build-info-additional") .build("bootBuildInfo"); - File file = new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties"); + File file = new File(this.gradleBuild.getProjectDir(), "build/bootBuildInfo/META-INF/build-info.properties"); assertThat(file).isFile(); Properties properties = buildInfoProperties(file); assertThat(properties).containsEntry("build.a", "alpha"); @@ -77,7 +77,7 @@ void buildInfoAdditional() { void buildInfoExcludeTime() { this.gradleBuild.script(Examples.DIR + "integrating-with-actuator/build-info-exclude-time") .build("bootBuildInfo"); - File file = new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties"); + File file = new File(this.gradleBuild.getProjectDir(), "build/bootBuildInfo/META-INF/build-info.properties"); assertThat(file).isFile(); Properties properties = buildInfoProperties(file); assertThat(properties).doesNotContainKey("build.time"); diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java index a9a3488d0efd..6de73280aea5 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/docs/PackagingDocumentationTests.java @@ -324,6 +324,13 @@ void bootBuildImageWithBindCaches() { .containsPattern("launchCache=/tmp/cache-gradle-[\\d]+.launch"); } + @TestTemplate + void bootBuildImageWithImageCaches() { + BuildResult result = this.gradleBuild.script(Examples.DIR + "packaging/boot-build-image-image-caches") + .build("bootBuildImageCaches"); + assertThat(result.getOutput()).containsPattern("buildCache=docker.io/library/gradle-[\\d]+:build"); + } + protected void jarFile(File file) throws IOException { try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file))) { jar.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF")); diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java index a0bfed5f8945..d354fcb175d1 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests.java @@ -20,6 +20,8 @@ import java.io.FileReader; import java.io.IOException; import java.util.Properties; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; import org.gradle.testkit.runner.BuildTask; import org.gradle.testkit.runner.TaskOutcome; @@ -44,8 +46,8 @@ class BuildInfoDslIntegrationTests { GradleBuild gradleBuild; @TestTemplate - void basicJar() { - BuildTask task = this.gradleBuild.build("bootBuildInfo", "--stacktrace").task(":bootBuildInfo"); + void basicJar() throws IOException { + BuildTask task = this.gradleBuild.build("bootJar").task(":bootBuildInfo"); assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Properties properties = buildInfoProperties(); @@ -53,6 +55,14 @@ void basicJar() { assertThat(properties).containsEntry("build.artifact", this.gradleBuild.getProjectDir().getName()); assertThat(properties).containsEntry("build.group", "com.example"); assertThat(properties).containsEntry("build.version", "1.0"); + File jar = new File(this.gradleBuild.getProjectDir(), "build/libs/").listFiles()[0]; + try (JarFile jarFile = new JarFile(jar)) { + JarEntry entry = jarFile.getJarEntry("META-INF/build-info.properties"); + assertThat(entry).isNotNull(); + Properties jarProperties = new Properties(); + jarProperties.load(jarFile.getInputStream(entry)); + assertThat(jarProperties).isEqualTo(properties); + } } @TestTemplate @@ -68,8 +78,8 @@ void jarWithCustomName() { } @TestTemplate - void basicWar() { - BuildTask task = this.gradleBuild.build("bootBuildInfo", "--stacktrace").task(":bootBuildInfo"); + void basicWar() throws IOException { + BuildTask task = this.gradleBuild.build("bootWar").task(":bootBuildInfo"); assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Properties properties = buildInfoProperties(); @@ -77,6 +87,14 @@ void basicWar() { assertThat(properties).containsEntry("build.artifact", this.gradleBuild.getProjectDir().getName()); assertThat(properties).containsEntry("build.group", "com.example"); assertThat(properties).containsEntry("build.version", "1.0"); + File war = new File(this.gradleBuild.getProjectDir(), "build/libs/").listFiles()[0]; + try (JarFile warFile = new JarFile(war)) { + JarEntry entry = warFile.getJarEntry("WEB-INF/classes/META-INF/build-info.properties"); + assertThat(entry).isNotNull(); + Properties jarProperties = new Properties(); + jarProperties.load(warFile.getInputStream(entry)); + assertThat(jarProperties).isEqualTo(properties); + } } @TestTemplate @@ -105,15 +123,8 @@ void additionalProperties() { assertThat(properties).containsEntry("build.b", "bravo"); } - @TestTemplate - void classesDependency() { - BuildTask task = this.gradleBuild.build("classes", "--stacktrace").task(":bootBuildInfo"); - assertThat(task).isNotNull(); - assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - } - private Properties buildInfoProperties() { - File file = new File(this.gradleBuild.getProjectDir(), "build/resources/main/META-INF/build-info.properties"); + File file = new File(this.gradleBuild.getProjectDir(), "build/bootBuildInfo/META-INF/build-info.properties"); assertThat(file).isFile(); Properties properties = new Properties(); try (FileReader reader = new FileReader(file)) { diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java index 67a0d22b0f04..9dccd61391c3 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/NativeImagePluginActionIntegrationTests.java @@ -47,23 +47,19 @@ @GradleCompatibility(minimumVersion = "8.3") class NativeImagePluginActionIntegrationTests { - private static final String NATIVE_BUILD_TOOLS_DEPRECATION = "Using a Project object as a dependency notation has been deprecated"; - @SuppressWarnings("NullAway.Init") GradleBuild gradleBuild; @TestTemplate void applyingNativeImagePluginAppliesAotPlugin() { - assertThat(this.gradleBuild.expectDeprecationMessages(NATIVE_BUILD_TOOLS_DEPRECATION) - .build("aotPluginApplied") - .getOutput()).contains("org.springframework.boot.aot applied = true"); + assertThat(this.gradleBuild.build("aotPluginApplied").getOutput()) + .contains("org.springframework.boot.aot applied = true"); } @TestTemplate void reachabilityMetadataConfigurationFilesAreCopiedToJar() throws IOException { writeDummySpringApplicationAotProcessorMainClass(); - BuildResult result = this.gradleBuild.expectDeprecationMessages(NATIVE_BUILD_TOOLS_DEPRECATION) - .build("bootJar"); + BuildResult result = this.gradleBuild.build("bootJar"); BuildTask task = result.task(":bootJar"); assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); @@ -71,11 +67,8 @@ void reachabilityMetadataConfigurationFilesAreCopiedToJar() throws IOException { File jarFile = new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(buildLibs.listFiles()).contains(jarFile); assertThat(getEntryNames(jarFile)).contains( - "META-INF/native-image/ch.qos.logback/logback-classic/1.2.11/reflect-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/jni-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/proxy-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/reflect-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/resource-config.json"); + "META-INF/native-image/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json", + "META-INF/native-image/org.jline/jline/3.21.0/reachability-metadata.json"); } @TestTemplate @@ -83,8 +76,7 @@ void reachabilityMetadataConfigurationFilesFromFileRepositoryAreCopiedToJar() th writeDummySpringApplicationAotProcessorMainClass(); FileSystemUtils.copyRecursively(new File("src/test/resources/reachability-metadata-repository"), new File(this.gradleBuild.getProjectDir(), "reachability-metadata-repository")); - BuildResult result = this.gradleBuild.expectDeprecationMessages(NATIVE_BUILD_TOOLS_DEPRECATION) - .build("bootJar"); + BuildResult result = this.gradleBuild.build("bootJar"); BuildTask task = result.task(":bootJar"); assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); @@ -92,33 +84,27 @@ void reachabilityMetadataConfigurationFilesFromFileRepositoryAreCopiedToJar() th File jarFile = new File(buildLibs, this.gradleBuild.getProjectDir().getName() + ".jar"); assertThat(buildLibs.listFiles()).contains(jarFile); assertThat(getEntryNames(jarFile)).contains( - "META-INF/native-image/ch.qos.logback/logback-classic/1.2.11/reflect-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/jni-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/proxy-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/reflect-config.json", - "META-INF/native-image/org.jline/jline/3.21.0/resource-config.json"); + "META-INF/native-image/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json", + "META-INF/native-image/org.jline/jline/3.21.0/reachability-metadata.json"); } @TestTemplate void developmentOnlyDependenciesDoNotAppearInNativeImageClasspath() { writeDummySpringApplicationAotProcessorMainClass(); - BuildResult result = this.gradleBuild.expectDeprecationMessages(NATIVE_BUILD_TOOLS_DEPRECATION) - .build("checkNativeImageClasspath"); + BuildResult result = this.gradleBuild.build("checkNativeImageClasspath"); assertThat(result.getOutput()).doesNotContain("commons-lang"); } @TestTemplate void testAndDevelopmentOnlyDependenciesDoNotAppearInNativeImageClasspath() { writeDummySpringApplicationAotProcessorMainClass(); - BuildResult result = this.gradleBuild.expectDeprecationMessages(NATIVE_BUILD_TOOLS_DEPRECATION) - .build("checkNativeImageClasspath"); + BuildResult result = this.gradleBuild.build("checkNativeImageClasspath"); assertThat(result.getOutput()).doesNotContain("commons-lang"); } @TestTemplate void classesGeneratedDuringAotProcessingAreOnTheNativeImageClasspath() { - BuildResult result = this.gradleBuild.expectDeprecationMessages(NATIVE_BUILD_TOOLS_DEPRECATION) - .build("checkNativeImageClasspath"); + BuildResult result = this.gradleBuild.build("checkNativeImageClasspath"); assertThat(result.getOutput()).contains(projectPath("build/classes/java/aot"), projectPath("build/resources/aot"), projectPath("build/generated/aotClasses")); } @@ -127,7 +113,6 @@ void classesGeneratedDuringAotProcessingAreOnTheNativeImageClasspath() { void classesGeneratedDuringAotTestProcessingAreOnTheTestNativeImageClasspath() { BuildResult result = this.gradleBuild .scriptProperty("junitVersion", TestTemplate.class.getPackage().getImplementationVersion()) - .expectDeprecationMessages(NATIVE_BUILD_TOOLS_DEPRECATION) .build("checkTestNativeImageClasspath"); assertThat(result.getOutput()).contains(projectPath("build/classes/java/aotTest"), projectPath("build/resources/aotTest"), projectPath("build/generated/aotTestClasses")); @@ -136,8 +121,7 @@ void classesGeneratedDuringAotTestProcessingAreOnTheTestNativeImageClasspath() { @TestTemplate void nativeEntryIsAddedToManifest() throws IOException { writeDummySpringApplicationAotProcessorMainClass(); - BuildResult result = this.gradleBuild.expectDeprecationMessages(NATIVE_BUILD_TOOLS_DEPRECATION) - .build("bootJar"); + BuildResult result = this.gradleBuild.build("bootJar"); BuildTask task = result.task(":bootJar"); assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java new file mode 100644 index 000000000000..aef986a3f756 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2012-present 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.gradle.plugin; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; + +import org.junit.jupiter.api.TestTemplate; + +import org.springframework.boot.gradle.junit.GradleCompatibility; +import org.springframework.boot.testsupport.gradle.testkit.GradleBuild; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link ProtobufPluginAction}. + * + * @author Andy Wilkinson + * @author Dongliang Xie + */ +@GradleCompatibility +class ProtobufPluginActionIntegrationTests { + + @SuppressWarnings("NullAway.Init") + GradleBuild gradleBuild; + + @TestTemplate + void configuresProtocArtifact() { + assertThat(this.gradleBuild.build("protocArtifact").getOutput()) + .contains("protoc artifact: 'com.google.protobuf:protoc'"); + } + + @TestTemplate + void configuresGrpcPlugin() { + assertThat(this.gradleBuild.build("grpcPlugin").getOutput()) + .contains("grpc plugin artifact: 'io.grpc:protoc-gen-grpc-java'"); + } + + @TestTemplate + void configuresGenerateProtoTasksToOmitGenerated() { + assertThat(this.gradleBuild.build("generateProtoTasksGrpcPluginOptions").getOutput()) + .contains("generateProto: [[@generated=omit]]") + .contains("generateTestProto: [[@generated=omit]]"); + } + + @TestTemplate + void alignsVersionOfProtocDependency() { + assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_protoc").getOutput()) + .contains("com.google.protobuf:protoc:null -> 4.34.0"); + } + + @TestTemplate + void generatesProtoSourcesWhenProtobufJavaProvidesProtocVersion() throws IOException { + File proto = new File(this.gradleBuild.getProjectDir(), "src/main/proto/example.proto"); + proto.getParentFile().mkdirs(); + Files.writeString(proto.toPath(), """ + syntax = "proto3"; + option java_package = "com.example"; + message Example { + string name = 1; + } + """); + this.gradleBuild.build("generateProto"); + } + + @TestTemplate + void alignsVersionOfGrpcDependency() { + assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_grpc").getOutput()) + .contains("io.grpc:protoc-gen-grpc-java:null -> 1.79.0"); + } + + @TestTemplate + void usesVersionOfProtocDependencyWhenSpecified() { + assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_protoc").getOutput()) + .contains("com.google.protobuf:protoc:4.33.5"); + } + + @TestTemplate + void usesVersionOfGrpcPluginDependencyWhenSpecified() { + assertThat(this.gradleBuild.build("dependencies", "--configuration", "protobufToolsLocator_grpc").getOutput()) + .contains("io.grpc:protoc-gen-grpc-java:1.78.0"); + + } + +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java index f87b87dc5e09..136f9462436f 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoIntegrationTests.java @@ -133,7 +133,8 @@ void reproducibleOutputWithFixedTime() throws IOException, InterruptedException BuildTask task = this.gradleBuild.build("buildInfo", "-PnullTime").task(":buildInfo"); assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); - File buildInfoProperties = new File(this.gradleBuild.getProjectDir(), "build/buildInfo/build-info.properties"); + File buildInfoProperties = new File(this.gradleBuild.getProjectDir(), + "build/buildInfo/META-INF/build-info.properties"); String firstHash = FileUtils.sha1Hash(buildInfoProperties); assertThat(buildInfoProperties.delete()).isTrue(); Thread.sleep(1500); @@ -157,7 +158,7 @@ void excludeProperties() { } private Properties buildInfoProperties() { - File file = new File(this.gradleBuild.getProjectDir(), "build/buildInfo/build-info.properties"); + File file = new File(this.gradleBuild.getProjectDir(), "build/buildInfo/META-INF/build-info.properties"); assertThat(file).isFile(); Properties properties = new Properties(); try (FileReader reader = new FileReader(file)) { diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java index 16cfa6140aef..a2048f3dc51c 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/buildinfo/BuildInfoTests.java @@ -171,6 +171,17 @@ void nullAdditionalPropertyProducesInformativeFailure() { .withMessage("Cannot add an entry with a null value to a property of type Map."); } + @Test + void filenameCanBeCustomized() { + Project project = createProject("test"); + BuildInfo task = createTask(project); + task.getFilename().set("custom-location-build-info.properties"); + task.generateBuildProperties(); + assertThat(new File(project.getLayout().getBuildDirectory().dir("testBuildInfo").get().getAsFile(), + "custom-location-build-info.properties")) + .exists(); + } + private Project createProject(String projectName) { File projectDir = new File(this.temp, projectName); return GradleProjectBuilder.builder().withProjectDir(projectDir).withName(projectName).build(); @@ -182,7 +193,8 @@ private BuildInfo createTask(Project project) { private Properties buildInfoProperties(BuildInfo task) { task.generateBuildProperties(); - return buildInfoProperties(new File(task.getDestinationDir().get().getAsFile(), "build-info.properties")); + return buildInfoProperties( + new File(task.getDestinationDir().get().getAsFile(), "META-INF/build-info.properties")); } private Properties buildInfoProperties(File file) { diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index bef260ab35af..01563aed94de 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -333,9 +333,9 @@ void implicitLayers() throws IOException { assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(toolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); @@ -360,7 +360,7 @@ void implicitLayers() throws IOException { expectedDependencies.add(this.libPath + "spring-boot-starter-logging-2.2.0.RELEASE.jar"); Set expectedSnapshotDependencies = new TreeSet<>(); expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + (toolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(toolsJar); assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); @@ -390,9 +390,9 @@ void multiModuleImplicitLayers() throws IOException { assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(toolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "bravo-1.2.3.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "charlie-1.2.3.jar")).isNotNull(); @@ -413,7 +413,7 @@ void multiModuleImplicitLayers() throws IOException { expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); Set expectedSnapshotDependencies = new TreeSet<>(); expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + (toolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(toolsJar); assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); assertThat(indexedLayers.get("spring-boot-loader")).containsExactly("org/"); assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); @@ -442,9 +442,9 @@ void customLayers() throws IOException { assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(toolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "spring-core-5.2.5.RELEASE.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "spring-jcl-5.2.5.RELEASE.jar")).isNotNull(); @@ -462,7 +462,7 @@ void customLayers() throws IOException { expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); List expectedSnapshotDependencies = new ArrayList<>(); expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + (toolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(toolsJar); assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); assertThat(indexedLayers.get("commons-dependencies")).containsExactly(this.libPath + "commons-lang3-3.9.jar"); assertThat(indexedLayers.get("snapshot-dependencies")).containsExactlyElementsOf(expectedSnapshotDependencies); @@ -495,9 +495,9 @@ void multiModuleCustomLayers() throws IOException { assertThat(task).isNotNull(); assertThat(task.getOutcome()).isEqualTo(TaskOutcome.SUCCESS); Map> indexedLayers; - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { - assertThat(jarFile.getEntry(layerToolsJar)).isNotNull(); + assertThat(jarFile.getEntry(toolsJar)).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "bravo-1.2.3.jar")).isNotNull(); assertThat(jarFile.getEntry(this.libPath + "charlie-1.2.3.jar")).isNotNull(); @@ -522,7 +522,7 @@ void multiModuleCustomLayers() throws IOException { expectedDependencies.add(this.libPath + "spring-jcl-5.2.5.RELEASE.jar"); List expectedSnapshotDependencies = new ArrayList<>(); expectedSnapshotDependencies.add(this.libPath + "library-1.0-SNAPSHOT.jar"); - (layerToolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(layerToolsJar); + (toolsJar.contains("SNAPSHOT") ? expectedSnapshotDependencies : expectedDependencies).add(toolsJar); assertThat(indexedLayers.get("subproject-dependencies")) .containsExactlyElementsOf(expectedSubprojectDependencies); assertThat(indexedLayers.get("dependencies")).containsExactlyElementsOf(expectedDependencies); diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java index 5ab4e610a866..10f9eaf5c003 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java @@ -488,22 +488,22 @@ void whenJarIsLayeredThenLayersIndexIsPresentAndCorrect() throws IOException { List index = entryLines(jarFile, this.indexPath + "layers.idx"); assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader", "snapshot-dependencies", "application"); - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); List expected = new ArrayList<>(); expected.add("- \"dependencies\":"); expected.add(" - \"" + this.libPath + "first-library.jar\""); expected.add(" - \"" + this.libPath + "first-project-library.jar\""); expected.add(" - \"" + this.libPath + "fourth-library.jar\""); expected.add(" - \"" + this.libPath + "second-library.jar\""); - if (!layerToolsJar.contains("SNAPSHOT")) { - expected.add(" - \"" + layerToolsJar + "\""); + if (!toolsJar.contains("SNAPSHOT")) { + expected.add(" - \"" + toolsJar + "\""); } expected.add("- \"spring-boot-loader\":"); expected.add(" - \"org/\""); expected.add("- \"snapshot-dependencies\":"); expected.add(" - \"" + this.libPath + "second-project-library-SNAPSHOT.jar\""); - if (layerToolsJar.contains("SNAPSHOT")) { - expected.add(" - \"" + layerToolsJar + "\""); + if (toolsJar.contains("SNAPSHOT")) { + expected.add(" - \"" + toolsJar + "\""); } expected.add(" - \"" + this.libPath + "third-library-SNAPSHOT.jar\""); expected.add("- \"application\":"); @@ -542,10 +542,10 @@ void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrect() th List index = entryLines(jarFile, this.indexPath + "layers.idx"); assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps", "resources", "application"); - String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); + String toolsJar = this.libPath + JarModeLibrary.TOOLS.getName(); List expected = new ArrayList<>(); expected.add("- \"my-deps\":"); - expected.add(" - \"" + layerToolsJar + "\""); + expected.add(" - \"" + toolsJar + "\""); expected.add("- \"my-internal-deps\":"); expected.add(" - \"" + this.libPath + "first-library.jar\""); expected.add(" - \"" + this.libPath + "first-project-library.jar\""); @@ -570,7 +570,7 @@ void whenJarIsLayeredWithCustomStrategiesThenLayersIndexIsPresentAndCorrect() th } @Test - void whenArchiveIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException { + void whenArchiveIsLayeredThenToolsJarIsIncluded() throws IOException { List entryNames = getEntryNames(createLayeredJar()); assertThat(entryNames).contains(this.libPath + JarModeLibrary.TOOLS.getName()); } diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java index 0eef7843ef36..ce72a74808e4 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootBuildImageTests.java @@ -29,6 +29,7 @@ import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.BuildpackReference; +import org.springframework.boot.buildpack.platform.build.Cache; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.ImagePlatform; import org.springframework.boot.buildpack.platform.docker.type.Binding; @@ -144,6 +145,25 @@ void whenTheEnvironmentIsSetItReplacesAnyExistingEntriesAndIsIncludedInTheReques .hasSize(2); } + @Test + void whenEnvironmentVariablesAreSetOnTheCommandLineTheyAreIncludedInTheRequest() { + this.buildImage.getEnvironmentFromCommandLine().add("ALPHA=a"); + this.buildImage.getEnvironmentFromCommandLine().add("BRAVO=b"); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "a") + .containsEntry("BRAVO", "b") + .hasSize(2); + } + + @Test + void environmentVariablesFromTheCommandLineOverrideThoseInTheBuildScript() { + this.buildImage.getEnvironment().put("ALPHA", "a"); + this.buildImage.getEnvironmentFromCommandLine().add("ALPHA=apple"); + this.buildImage.getEnvironmentFromCommandLine().add("BRAVO=banana"); + assertThat(this.buildImage.createRequest().getEnv()).containsEntry("ALPHA", "apple") + .containsEntry("BRAVO", "banana") + .hasSize(2); + } + @Test void whenUsingDefaultConfigurationThenRequestHasVerboseLoggingDisabled() { assertThat(this.buildImage.createRequest().isVerboseLogging()).isFalse(); @@ -338,4 +358,28 @@ void whenImagePlatformIsConfiguredThenRequestHasImagePlatform() { assertThat(this.buildImage.createRequest().getImagePlatform()).isEqualTo(ImagePlatform.of("linux/arm64/v1")); } + @Test + void whenBuildWorkspaceIsConfiguredThenRequestHasBuildWorkspace() { + this.buildImage.buildWorkspace((cache) -> cache.volume((volume) -> volume.getName().set("workspace-vol"))); + assertThat(this.buildImage.createRequest().getBuildWorkspace()).isEqualTo(Cache.volume("workspace-vol")); + } + + @Test + void whenBuildCacheIsConfiguredWithVolumeThenRequestHasBuildCache() { + this.buildImage.buildCache((cache) -> cache.volume((volume) -> volume.getName().set("build-vol"))); + assertThat(this.buildImage.createRequest().getBuildCache()).isEqualTo(Cache.volume("build-vol")); + } + + @Test + void whenBuildCacheIsConfiguredWithImageThenRequestHasBuildCache() { + this.buildImage.buildCache((cache) -> cache.image((image) -> image.getName().set("build-cache-image"))); + assertThat(this.buildImage.createRequest().getBuildCache()).isEqualTo(Cache.image("build-cache-image")); + } + + @Test + void whenLaunchCacheIsConfiguredThenRequestHasLaunchCache() { + this.buildImage.launchCache((cache) -> cache.bind((bind) -> bind.getSource().set("/tmp/launch"))); + assertThat(this.buildImage.createRequest().getLaunchCache()).isEqualTo(Cache.bind("/tmp/launch")); + } + } diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java index b064ba83f0e1..d11755d68d68 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/testkit/PluginClasspathGradleBuild.java @@ -22,6 +22,7 @@ import java.util.List; import com.fasterxml.jackson.annotation.JsonView; +import com.google.protobuf.gradle.ProtobufPlugin; import com.sun.jna.Platform; import io.spring.gradle.dependencymanagement.DependencyManagementPlugin; import org.antlr.v4.runtime.Lexer; @@ -32,7 +33,6 @@ import org.cyclonedx.gradle.CyclonedxPlugin; import org.gradle.testkit.runner.GradleRunner; import org.jetbrains.kotlin.gradle.fus.BuildUidService; -import org.jetbrains.kotlin.gradle.model.KotlinProject; import org.jetbrains.kotlin.gradle.plugin.KotlinCompilerPluginSupportPlugin; import org.jetbrains.kotlin.project.model.LanguageSettings; import org.jetbrains.kotlin.tooling.core.KotlinToolingVersion; @@ -86,12 +86,10 @@ private List pluginClasspath() { classpath.add(new File(pathOfJarContaining(DependencyManagementPlugin.class))); if (this.kotlin) { classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.cli.common.PropertiesKt"))); - classpath.add(new File(pathOfJarContaining(KotlinProject.class))); classpath.add(new File(pathOfJarContaining(KotlinToolingVersion.class))); - classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.build.report.metrics.BuildTime"))); + classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.build.report.metrics.BuildTimes"))); classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.buildtools.api.CompilationService"))); classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.daemon.client.KotlinCompilerClient"))); - classpath.add(new File(pathOfJarContaining("org.jetbrains.kotlin.konan.library.KonanLibrary"))); classpath.add(new File(pathOfJarContaining(KotlinCompilerPluginSupportPlugin.class))); classpath.add(new File(pathOfJarContaining(LanguageSettings.class))); classpath.add(new File(pathOfJarContaining(BuildUidService.class))); @@ -127,6 +125,10 @@ private List pluginClasspath() { classpath.add(new File(pathOfJarContaining("org.codehaus.plexus.util.xml.pull.XmlPullParserException"))); classpath.add(new File(pathOfJarContaining("org.codehaus.stax2.ri.Stax2WriterAdapter"))); classpath.add(new File(pathOfJarContaining("org.cyclonedx.model.ExternalReference"))); + // Protobuf dependencies + classpath.add(new File(pathOfJarContaining(ProtobufPlugin.class))); + classpath.add(new File(pathOfJarContaining("com.google.gradle.osdetector.OsDetectorPlugin"))); + classpath.add(new File(pathOfJarContaining("kr.motd.maven.os.FileOperationProvider"))); return classpath; } diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicJar.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicJar.gradle index f0199355d29a..736b2fedb28f 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicJar.gradle +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicJar.gradle @@ -24,4 +24,7 @@ version = '1.0' springBoot { buildInfo() + mainClass = "com.example.Main" } + + diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicWar.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicWar.gradle index 463b56870aed..e2b50c30f199 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicWar.gradle +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/dsl/BuildInfoDslIntegrationTests-basicWar.gradle @@ -24,4 +24,5 @@ version = '1.0' springBoot { buildInfo() + mainClass = "com.example.Main" } diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfGrpcPluginDependencyWhenSpecified.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfGrpcPluginDependencyWhenSpecified.gradle new file mode 100644 index 000000000000..e78f2702a5a7 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfGrpcPluginDependencyWhenSpecified.gradle @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present 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. + */ + +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +apply plugin: 'com.google.protobuf' + +group = 'com.example' +version = '0.0.1' + +repositories { + mavenCentral() +} + +protobuf { + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:1.78.0" + } + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfProtocDependencyWhenSpecified.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfProtocDependencyWhenSpecified.gradle new file mode 100644 index 000000000000..8799971a3929 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests-usesVersionOfProtocDependencyWhenSpecified.gradle @@ -0,0 +1,35 @@ +/* + * Copyright 2012-present 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. + */ + +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +apply plugin: 'com.google.protobuf' + +group = 'com.example' +version = '0.0.1' + +repositories { + mavenCentral() +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:4.33.5" + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle new file mode 100644 index 000000000000..4eb508578b99 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/ProtobufPluginActionIntegrationTests.gradle @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present 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. + */ + +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +apply plugin: 'com.google.protobuf' + +group = 'com.example' +version = '0.0.1' + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.google.protobuf:protobuf-java:4.34.0") + implementation("io.grpc:grpc-util:1.79.0") +} + +protobuf { + plugins { + grpc {} + } +} + +tasks.register("protocArtifact") { + doFirst { + protobuf { + protoc { + println "protoc artifact: '$artifact'" + } + } + } +} + +tasks.register("grpcPlugin") { + doFirst { + protobuf { + plugins { + grpc { + println "grpc plugin artifact: '$artifact'" + } + } + } + } +} + +tasks.register("generateProtoTasksGrpcPluginOptions") { + doFirst { + tasks.withType(com.google.protobuf.gradle.GenerateProtoTask).each { println "${it.name}: ${it.pluginsForCaching.collect { it.options }}" } + } +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/index.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/index.json deleted file mode 100644 index 768b5502a0b7..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/index.json +++ /dev/null @@ -1,3 +0,0 @@ -[ - "reflect-config.json" -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json new file mode 100644 index 000000000000..d42db680a786 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reachability-metadata.json @@ -0,0 +1,297 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.CallerDataConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.ClassOfCallerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.ContextNameConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.DateConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.FileOfCallerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.LevelConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.LineOfCallerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.LineSeparatorConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.LocalSequenceNumberConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.LoggerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.MDCConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.MarkerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.MessageConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.MethodOfCallerConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.NopThrowableInformationConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.PrefixCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.PropertyConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.RelativeTimeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.ThreadConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.ThrowableProxyConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.IdentityCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.ReplacingCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BlackCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BlueCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldBlueCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldCyanCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldGreenCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldMagentaCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldRedCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.BoldYellowCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.CyanCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.GrayCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.GreenCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.MagentaCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.RedCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.WhiteCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "ch.qos.logback.core.pattern.color.YellowCompositeConverter", + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "ch.qos.logback.classic.LoggerContext" + }, + "type": "org.slf4j.impl.StaticLoggerBinder" + } + ] +} \ No newline at end of file diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reflect-config.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reflect-config.json deleted file mode 100644 index a0856ee205ba..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/ch.qos.logback/logback-classic/1.2.11/reflect-config.json +++ /dev/null @@ -1,299 +0,0 @@ -[ - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.CallerDataConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.CallerDataConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.ClassOfCallerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.ContextNameConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.DateConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.FileOfCallerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.LevelConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.LineOfCallerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.LineSeparatorConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.LocalSequenceNumberConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.LoggerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.MDCConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.MarkerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.MessageConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.MethodOfCallerConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.NopThrowableInformationConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.PrefixCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.PropertyConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.RelativeTimeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.ThreadConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.ThrowableProxyConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.classic.pattern.color.HighlightingCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.IdentityCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.ReplacingCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BlackCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BlueCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldBlueCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldCyanCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldGreenCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldMagentaCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldRedCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldWhiteCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.BoldYellowCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.CyanCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.GrayCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.GreenCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.MagentaCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.RedCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.WhiteCompositeConverter", - "allPublicConstructors": true - }, - { - "condition": { - "typeReachable": "ch.qos.logback.classic.LoggerContext" - }, - "name": "ch.qos.logback.core.pattern.color.YellowCompositeConverter", - "allPublicConstructors": true - }, - { - "name": "org.slf4j.impl.StaticLoggerBinder" - } -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/index.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/index.json deleted file mode 100644 index 554ddf047aa7..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/index.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - "jni-config.json", - "proxy-config.json", - "reflect-config.json", - "resource-config.json" -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/jni-config.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/jni-config.json deleted file mode 100644 index 85d5b8db21cc..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/jni-config.json +++ /dev/null @@ -1,128 +0,0 @@ -[ - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiNativePty" - }, - "fields": [ - { - "name": "HAVE_ISATTY" - }, - { - "name": "HAVE_TTYNAME" - }, - { - "name": "TCSADRAIN" - }, - { - "name": "TCSAFLUSH" - }, - { - "name": "TCSANOW" - }, - { - "name": "TIOCGETD" - }, - { - "name": "TIOCGWINSZ" - }, - { - "name": "TIOCSETD" - }, - { - "name": "TIOCSWINSZ" - } - ], - "name": "org.fusesource.jansi.internal.CLibrary" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.CLibrary$WinSize" - }, - "name": "org.fusesource.jansi.internal.CLibrary$WinSize" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.CLibrary$Termios" - }, - "name": "org.fusesource.jansi.internal.CLibrary$Termios" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32" - }, - "name": "org.fusesource.jansi.internal.Kernel32" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$SMALL_RECT" - }, - "name": "org.fusesource.jansi.internal.Kernel32$SMALL_RECT" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$COORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$COORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$CONSOLE_SCREEN_BUFFER_INFO" - }, - "name": "org.fusesource.jansi.internal.Kernel32$CONSOLE_SCREEN_BUFFER_INFO" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$CHAR_INFO" - }, - "name": "org.fusesource.jansi.internal.Kernel32$CHAR_INFO" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$KEY_EVENT_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$KEY_EVENT_RECORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$MOUSE_EVENT_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$MOUSE_EVENT_RECORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$WINDOW_BUFFER_SIZE_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$WINDOW_BUFFER_SIZE_RECORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$FOCUS_EVENT_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$FOCUS_EVENT_RECORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$MENU_EVENT_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$MENU_EVENT_RECORD" - }, - { - "allDeclaredFields": true, - "condition": { - "typeReachable": "org.fusesource.jansi.internal.Kernel32$INPUT_RECORD" - }, - "name": "org.fusesource.jansi.internal.Kernel32$INPUT_RECORD" - } -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/proxy-config.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/proxy-config.json deleted file mode 100644 index 30918f9df6f4..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/proxy-config.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "condition": { - "typeReachable": "org.jline.utils.Signals" - }, - "interfaces": [ - "sun.misc.SignalHandler" - ] - } -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reachability-metadata.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reachability-metadata.json new file mode 100644 index 000000000000..9e039ee47e03 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reachability-metadata.json @@ -0,0 +1,380 @@ +{ + "reflection": [ + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiNativePty" + }, + "type": "java.io.FileDescriptor" + }, + { + "condition": { + "typeReached": "org.jline.terminal.TerminalBuilder" + }, + "type": "java.lang.ProcessHandle", + "methods": [ + { + "name": "current", + "parameterTypes": [] + }, + { + "name": "info", + "parameterTypes": [] + }, + { + "name": "parent", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReached": "org.jline.terminal.TerminalBuilder" + }, + "type": "java.lang.ProcessHandle$Info", + "methods": [ + { + "name": "command", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiNativePty" + }, + "type": "org.fusesource.jansi.internal.CLibrary", + "jniAccessible": true, + "fields": [ + { + "name": "HAVE_ISATTY" + }, + { + "name": "HAVE_TTYNAME" + }, + { + "name": "TCSADRAIN" + }, + { + "name": "TCSAFLUSH" + }, + { + "name": "TCSANOW" + }, + { + "name": "TIOCGETD" + }, + { + "name": "TIOCGWINSZ" + }, + { + "name": "TIOCSETD" + }, + { + "name": "TIOCSWINSZ" + } + ] + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.CLibrary$Termios" + }, + "type": "org.fusesource.jansi.internal.CLibrary$Termios", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.CLibrary$WinSize" + }, + "type": "org.fusesource.jansi.internal.CLibrary$WinSize", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32" + }, + "type": "org.fusesource.jansi.internal.Kernel32", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$CHAR_INFO" + }, + "type": "org.fusesource.jansi.internal.Kernel32$CHAR_INFO", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$CONSOLE_SCREEN_BUFFER_INFO" + }, + "type": "org.fusesource.jansi.internal.Kernel32$CONSOLE_SCREEN_BUFFER_INFO", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$COORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$COORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$FOCUS_EVENT_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$FOCUS_EVENT_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$INPUT_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$INPUT_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$KEY_EVENT_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$KEY_EVENT_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$MENU_EVENT_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$MENU_EVENT_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$MOUSE_EVENT_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$MOUSE_EVENT_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$SMALL_RECT" + }, + "type": "org.fusesource.jansi.internal.Kernel32$SMALL_RECT", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.fusesource.jansi.internal.Kernel32$WINDOW_BUFFER_SIZE_RECORD" + }, + "type": "org.fusesource.jansi.internal.Kernel32$WINDOW_BUFFER_SIZE_RECORD", + "allDeclaredFields": true, + "jniAccessible": true + }, + { + "condition": { + "typeReached": "org.jline.builtins.Styles" + }, + "type": "org.jline.console.SystemRegistry", + "methods": [ + { + "name": "get", + "parameterTypes": [] + } + ] + }, + { + "condition": { + "typeReached": "org.jline.utils.Signals" + }, + "type": "sun.misc.Signal", + "methods": [ + { + "name": "", + "parameterTypes": [ + "java.lang.String" + ] + }, + { + "name": "handle", + "parameterTypes": [ + "sun.misc.Signal", + "sun.misc.SignalHandler" + ] + } + ] + }, + { + "condition": { + "typeReached": "org.jline.terminal.TerminalBuilder" + }, + "type": "sun.misc.SignalHandler", + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredConstructors": true, + "allPublicConstructors": true + }, + { + "condition": { + "typeReached": "org.jline.utils.Signals" + }, + "type": "sun.misc.SignalHandler", + "fields": [ + { + "name": "SIG_DFL" + } + ] + }, + { + "condition": { + "typeReached": "org.jline.utils.Signals" + }, + "type": { + "proxy": [ + "sun.misc.SignalHandler" + ] + } + } + ], + "resources": [ + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiNativePty" + }, + "glob": "META-INF/native/windows64/jansi.dll" + }, + { + "condition": { + "typeReached": "org.jline.terminal.TerminalBuilder" + }, + "glob": "META-INF/services/org.jline.terminal.spi.JansiSupport" + }, + { + "condition": { + "typeReached": "org.jline.terminal.TerminalBuilder" + }, + "glob": "META-INF/services/org.jline.terminal.spi.JnaSupport" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiNativePty" + }, + "glob": "org/fusesource/jansi/internal/native/Linux/x86_64/libjansi.so" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiNativePty" + }, + "glob": "org/fusesource/jansi/jansi.properties" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.jansi.JansiSupportImpl" + }, + "glob": "org/fusesource/jansi/jansi.properties" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/ansi.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/capabilities.txt" + }, + { + "condition": { + "typeReached": "org.jline.utils.Colors" + }, + "glob": "org/jline/utils/colors.txt" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.DumbTerminal" + }, + "glob": "org/jline/utils/dumb-color.caps" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.DumbTerminal" + }, + "glob": "org/jline/utils/dumb.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/rxvt-basic.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/rxvt-unicode-256color.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/rxvt-unicode.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/rxvt.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/screen-256color.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/screen.caps" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.AbstractWindowsTerminal" + }, + "glob": "org/jline/utils/windows-256color.caps" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.AbstractWindowsTerminal" + }, + "glob": "org/jline/utils/windows-conemu.caps" + }, + { + "condition": { + "typeReached": "org.jline.terminal.impl.AbstractWindowsTerminal" + }, + "glob": "org/jline/utils/windows.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/xterm-256color.caps" + }, + { + "condition": { + "typeReached": "org.jline.utils.InfoCmp" + }, + "glob": "org/jline/utils/xterm.caps" + } + ] +} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reflect-config.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reflect-config.json deleted file mode 100644 index 665ebda94772..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/reflect-config.json +++ /dev/null @@ -1,104 +0,0 @@ -[ - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiNativePty" - }, - "name": "java.io.FileDescriptor", - "queriedMethods": [ - { - "name": "", - "parameterTypes": [ - "int" - ] - } - ] - }, - { - "condition": { - "typeReachable": "org.jline.terminal.TerminalBuilder" - }, - "methods": [ - { - "name": "current", - "parameterTypes": [] - }, - { - "name": "info", - "parameterTypes": [] - }, - { - "name": "parent", - "parameterTypes": [] - } - ], - "name": "java.lang.ProcessHandle" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.TerminalBuilder" - }, - "methods": [ - { - "name": "command", - "parameterTypes": [] - } - ], - "name": "java.lang.ProcessHandle$Info" - }, - { - "condition": { - "typeReachable": "org.jline.builtins.Styles" - }, - "methods": [ - { - "name": "get", - "parameterTypes": [] - } - ], - "name": "org.jline.console.SystemRegistry" - }, - { - "condition": { - "typeReachable": "org.jline.utils.Signals" - }, - "methods": [ - { - "name": "", - "parameterTypes": [ - "java.lang.String" - ] - }, - { - "name": "handle", - "parameterTypes": [ - "sun.misc.Signal", - "sun.misc.SignalHandler" - ] - } - ], - "name": "sun.misc.Signal" - }, - { - "condition": { - "typeReachable": "org.jline.utils.Signals" - }, - "fields": [ - { - "name": "SIG_DFL" - } - ], - "name": "sun.misc.SignalHandler" - }, - { - "allDeclaredClasses": true, - "allDeclaredConstructors": true, - "allDeclaredMethods": true, - "allPublicClasses": true, - "allPublicConstructors": true, - "allPublicMethods": true, - "condition": { - "typeReachable": "org.jline.terminal.TerminalBuilder" - }, - "name": "sun.misc.SignalHandler" - } -] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/resource-config.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/resource-config.json deleted file mode 100644 index 65fed15e1053..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/org.jline/jline/3.21.0/resource-config.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "bundles": [], - "resources": { - "includes": [ - { - "condition": { - "typeReachable": "org.jline.terminal.TerminalBuilder" - }, - "pattern": "\\QMETA-INF/services/org.jline.terminal.spi.JansiSupport\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.TerminalBuilder" - }, - "pattern": "\\QMETA-INF/services/org.jline.terminal.spi.JnaSupport\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiNativePty" - }, - "pattern": "\\Qorg/fusesource/jansi/internal/native/Linux/x86_64/libjansi.so\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiNativePty" - }, - "pattern": "\\QMETA-INF/native/windows64/jansi.dll\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiNativePty" - }, - "pattern": "\\Qorg/fusesource/jansi/jansi.properties\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.jansi.JansiSupportImpl" - }, - "pattern": "\\Qorg/fusesource/jansi/jansi.properties\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/capabilities.txt\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.Colors" - }, - "pattern": "\\Qorg/jline/utils/colors.txt\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/ansi.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.DumbTerminal" - }, - "pattern": "\\Qorg/jline/utils/dumb.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.DumbTerminal" - }, - "pattern": "\\Qorg/jline/utils/dumb-color.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/rxvt.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/rxvt-basic.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/rxvt-unicode.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/rxvt-unicode-256color.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/screen.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/screen-256color.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.AbstractWindowsTerminal" - }, - "pattern": "\\Qorg/jline/utils/windows.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.AbstractWindowsTerminal" - }, - "pattern": "\\Qorg/jline/utils/windows-256color.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.terminal.impl.AbstractWindowsTerminal" - }, - "pattern": "\\Qorg/jline/utils/windows-conemu.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/xterm.caps\\E" - }, - { - "condition": { - "typeReachable": "org.jline.utils.InfoCmp" - }, - "pattern": "\\Qorg/jline/utils/xterm-256color.caps\\E" - } - ] - } -} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v1.0.0.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v2.0.0.json similarity index 61% rename from build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v1.0.0.json rename to build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v2.0.0.json index 2461661166eb..ef7edc27ba2c 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v1.0.0.json +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-library-index-schema-v2.0.0.json @@ -1,5 +1,5 @@ { - "$id": "https://raw.githubusercontent.com/oracle/graalvm-reachability-metadata/master/metadata/schemas/metadata-library-index-schema-v1.0.0.json", + "$id": "https://raw.githubusercontent.com/oracle/graalvm-reachability-metadata/master/metadata/schemas/metadata-library-index-schema-v2.0.0.json", "$schema": "https://json-schema.org/draft/2020-12/schema", "description": "Schema for metadata///index.json. Each entry describes a metadata bundle for a range of library versions.", "type": "array", @@ -8,15 +8,11 @@ "type": "object", "additionalProperties": false, "required": [ - "module", + "allowed-packages", "metadata-version", "tested-versions" ], "properties": { - "module": { - "$ref": "#/$defs/moduleCoordinate", - "description": "Maven coordinates in the form ':'." - }, "metadata-version": { "type": "string", "minLength": 1, @@ -37,6 +33,26 @@ "minLength": 1, "description": "Overrides 'metadata-version' for use in test directories. This allows one metadata entry to share tests defined for a different version's metadata directory." }, + "source-code-url": { + "type": "string", + "minLength": 1, + "description": "Optional URL pointing to the metadata source code for this entry." + }, + "repository-url": { + "type": "string", + "minLength": 1, + "description": "Optional URL pointing to the canonical project repository for this entry." + }, + "documentation-url": { + "type": "string", + "minLength": 1, + "description": "Optional URL pointing to the project documentation for this entry." + }, + "test-code-url": { + "type": "string", + "minLength": 1, + "description": "Optional URL pointing to the test source code for this entry." + }, "latest": { "type": "boolean", "description": "Marks this entry as the latest/default metadata for currently supported versions." @@ -49,6 +65,16 @@ "type": "boolean", "description": "When true, expresses the intent to exclude outdated built-in metadata shipped with Native Image for the matched versions." }, + "allowed-packages": { + "type": "array", + "description": "Packages expected to contain metadata entries for this library. Used by CI checks.", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, "skipped-versions": { "type": "array", "description": "Versions explicitly excluded from support, each with a reason.", @@ -68,6 +94,15 @@ } } } + }, + "requires": { + "type": "array", + "description": "List of dependent modules (group:artifact) whose metadata is required at runtime.", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/moduleCoordinate" + } } } }, @@ -81,22 +116,26 @@ [ { "metadata-version": "0.0.1", - "module": "org.example:library", "tested-versions": [ "0.0.1", "0.0.2" ], "default-for": "0\\.0\\..*", - "test-version": "0.0.1" + "test-version": "0.0.1", + "source-code-url": "https://github.com/example/project/tree/main/metadata/org.example/library/0.0.1", + "repository-url": "https://github.com/example/project", + "test-code-url": "https://github.com/example/project/tree/main/tests/src/org.example/library/0.0.1", + "documentation-url": "https://github.com/example/project/tree/main/README.md", + "allowed-packages": [ "org.example.library" ] }, { "latest": true, "metadata-version": "1.0.0", - "module": "org.example:library", - "tested-versions": [ "1.0.0", "1.1.0" , "1.2.0"] + "tested-versions": [ "1.0.0", "1.1.0" , "1.2.0"], + "allowed-packages": [ "org.example.library" ] }, { "metadata-version": "1.19.0", - "module": "io.opentelemetry:opentelemetry-sdk-trace", "tested-versions": [ "1.19.0" ], "override": true, + "allowed-packages": [ "io.opentelemetry" ], "skipped-versions": [ { "version": "1.34.0", "reason": "Dependency io.opentelemetry:opentelemetry-api:1.34.0 provides reflect-config.json which does not parse." } ] diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json deleted file mode 100644 index 7087aaa1d29c..000000000000 --- a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/metadata-root-index-schema-v1.0.0.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "$id": "https://raw.githubusercontent.com/oracle/graalvm-reachability-metadata/master/metadata/schemas/metadata-root-index-schema-v1.0.0.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "Schema for metadata/index.json. This file lists modules known to the repository and optionally the directory where their metadata is stored, the allowed package prefixes for metadata, and inter-module requirements. See docs/CONTRIBUTING.md (Metadata structure).", - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["module"], - "properties": { - "module": { - "$ref": "#/$defs/moduleCoordinate", - "description": "Maven coordinates for the module in the form ':'." - }, - "directory": { - "type": "string", - "minLength": 1, - "pattern": "^[^\\s].*$", - "description": "Repository-relative path under 'metadata/' containing this module's metadata (e.g. 'org.example/library'). If omitted, the entry may reference requirements only." - }, - "allowed-packages": { - "type": "array", - "description": "List of package (or fully-qualified name) prefixes considered valid sources of metadata entries for this module. Used to filter-in relevant JSON entries.", - "minItems": 1, - "uniqueItems": true, - "items": { - "type": "string", - "minLength": 1 - } - }, - "requires": { - "type": "array", - "description": "Optional list of module coordinates this module depends on. Each item is ':'.", - "minItems": 1, - "uniqueItems": true, - "items": { - "$ref": "#/$defs/moduleCoordinate" - } - } - } - }, - "$defs": { - "moduleCoordinate": { - "type": "string", - "pattern": "^[^:]+:[^:]+$" - } - }, - "examples": [ - [ - { - "directory": "org.example/library", - "module": "org.example:library" - }, - { - "allowed-packages": ["org.package.name"], - "module": "org.example:dependant-library", - "requires": ["org.example:library"] - }, - { - "allowed-packages" : [ "org.hibernate", "jakarta" ], - "directory" : "org.hibernate.orm/hibernate-envers", - "module" : "org.hibernate.orm:hibernate-envers", - "requires" : [ "org.hibernate.orm:hibernate-core" ] - } - ] - ] -} diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/reachability-metadata-schema-v1.2.0.json b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/reachability-metadata-schema-v1.2.0.json new file mode 100644 index 000000000000..ef4ca27a589e --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/reachability-metadata-repository/schemas/reachability-metadata-schema-v1.2.0.json @@ -0,0 +1,458 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/assets/reachability-metadata-schema-v1.2.0.json", + "title": "JSON schema for the reachability metadata used by GraalVM Native Image", + "version": "1.2.0", + "type": "object", + "default": {}, + "properties": { + "comment": { + "title": "Comment(s) applying to the whole file (e.g., generation date, author, etc.)", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "default": "" + }, + "reflection": { + "title": "Metadata to ensure elements are accessible via reflection", + "type": "array", + "default": [], + "items": { + "title": "Elements that should be registered for reflection for a specified type", + "type": "object", + "required": ["type"], + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "type": { + "title": "Type descriptor of the class that should be registered for reflection", + "$ref": "#/$defs/type" + }, + "methods": { + "title": "List of methods that should be registered for the type declared in ", + "type": "array", + "default": [], + "items": { + "title": "Method descriptor of the method that should be registered for reflection", + "$ref": "#/$defs/method" + } + }, + "fields": { + "title": "List of class fields that can be read or written to for the type declared in ", + "type": "array", + "default": [], + "items": { + "title": "Field descriptor of the field that should be registered for reflection", + "type": "object", + "properties": { + "name": { + "title": "Name of the field that should be registered for reflection", + "type": "string", + "pattern": "^[^.;\\[/]+$" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "allDeclaredMethods": { + "title": "Register all declared methods from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "allDeclaredFields": { + "title": "Register all declared fields from the type for reflective access", + "type": "boolean", + "default": false + }, + "allDeclaredConstructors": { + "title": "Register all declared constructors from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "allPublicMethods": { + "title": "Register all public methods from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "allPublicFields": { + "title": "Register all public fields from the type for reflective access", + "type": "boolean", + "default": false + }, + "allPublicConstructors": { + "title": "Register all public constructors from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "unsafeAllocated": { + "title": "Allow objects of this class to be instantiated with a call to jdk.internal.misc.Unsafe#allocateInstance or JNI's AllocObject", + "type": "boolean", + "default": false + }, + "serializable": { + "title": "Allow objects of this class to be serialized and deserialized", + "type": "boolean", + "default": false + }, + "jniAccessible": { + "title": "Register the type, including all registered fields and methods, for runtime JNI access", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + }, + "resources": { + "title": "Metadata to ensure resources are accessible", + "type": "array", + "default": [], + "items": { + "oneOf": [ + { + "title": "Resource that should be accessible", + "type": "object", + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "module": { + "title": "Module containing the resource", + "type": "string", + "default": "" + }, + "glob": { + "title": "Resource name or pattern matching multiple resources (accepts * and ** wildcards)", + "type": "string" + } + }, + "required": [ + "glob" + ], + "additionalProperties": false + }, + { + "title": "Resource bundle that should be available", + "type": "object", + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "module": { + "title": "Module containing the resource bundle", + "type": "string", + "default": "" + }, + "bundle": { + "title": "Resource bundle name", + "type": "string" + } + }, + "required": [ + "bundle" + ], + "additionalProperties": false + } + ] + } + }, + "foreign": { + "properties": { + "downcalls": { + "default": [], + "items": { + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "returnType": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "parameterTypes": { + "default": [], + "items": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "type": "array", + "title": "List of the function descriptor's parameter types" + }, + "options": { + "type": "object", + "title": "Linker options (see `java.lang.foreign.Linker.Option`)", + "properties": { + "captureCallState": { + "type": "boolean", + "title": "Specifies whether a call state should be captured. The specific states to capture are determined at run time. See also: `java.lang.foreign.Linker.Option.captureCallState`" + }, + "critical": { + "type": "object", + "title": "See `java.lang.foreign.Linker.Option.critical`", + "properties": { + "allowHeapAccess": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "firstVariadicArg": { + "type": "integer", + "title": "See `java.lang.foreign.Linker.Option.firstVariadicArg`" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": ["returnType", "parameterTypes"], + "type": "object", + "title": "Function descriptor to be registered for a downcall" + }, + "type": "array", + "title": "List of function descriptors that should be registered for downcalls" + }, + "upcalls": { + "default": [], + "items": { + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "returnType": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "parameterTypes": { + "default": [], + "items": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "type": "array", + "title": "List of the function descriptor's parameter types" + }, + "options": { + "type": "object", + "title": "Linker options (see `java.lang.foreign.Linker.Option`)", + "description": "Currently, no linker options are allowed for upcalls. This may change in the future.", + "properties": { }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": ["returnType", "parameterTypes"], + "type": "object", + "title": "Function descriptor to be registered for an upcall" + }, + "type": "array", + "title": "List of function descriptors that should be registered for upcalls" + }, + "directUpcalls": { + "default": [], + "items": { + "properties": { + "reason": { + "$ref": "#/$defs/reason" + }, + "condition": { + "$ref": "#/$defs/condition" + }, + "class": { + "$ref": "#/$defs/className", + "title": "Fully-qualified class name (e.g., `org.package.OuterClass$InnerClass`)" + }, + "method": { + "type": "string", + "title": "Method name" + }, + "returnType": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "parameterTypes": { + "default": [], + "items": { + "type": "string", + "title": "Memory layout definition (allows canonical layouts; see `java.lang.foreign.Linker`)" + }, + "type": "array", + "title": "List of the function descriptor's parameter types" + }, + "options": { + "type": "object", + "title": "Linker options (see `java.lang.foreign.Linker.Option`)", + "description": "Currently, no linker options are allowed for direct upcalls. This may change in the future.", + "properties": { }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": ["class", "method"], + "type": "object", + "title": "Java method and function descriptor to be registered for a direct upcall" + }, + "type": "array", + "title": "List of Java methods and function descriptors that should be registered for direct upcalls" + } + }, + "type": "object", + "additionalProperties": false, + "title": "JSON schema for the FFM API configuration used by GraalVM Native Image", + "description": "For a description and examples of writing an FFM API configuration, see: https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/FFM-API.md" + } + }, + "required": [], + "additionalProperties": false, + + "$defs": { + "className": { + "type": "string", + "pattern": "^[^.;\\[/]+(\\.[^.;\\[/]+)*$" + }, + "typeName": { + "type": "string", + "pattern": "^[^.;\\[/]+(\\.[^.;\\[/]+)*(\\[])*$" + }, + "reason": { + "title": "Reason(s) for including this element (e.g., needed for establishing connections, or needed by method X#Y for task Z)", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "default": "" + }, + "condition": { + "title": "Condition used to determine if the element will be accessible at run time", + "type": "object", + "properties": { + "typeReached": { + "title": "Type descriptor of a class that must be reached in order to allow access to the element", + "$ref": "#/$defs/type" + } + }, + "required": [ + "typeReached" + ], + "additionalProperties": false + }, + "type": { + "title": "Type descriptors used by GraalVM Native Image metadata files", + "oneOf": [ + { + "$ref": "#/$defs/typeName" + }, + { + "type": "object", + "properties": { + "proxy": { + "title": "A list of interfaces defining the proxy class", + "type": "array", + "items": { + "title": "Fully-qualified name of the interface defining the proxy class", + "$ref": "#/$defs/className" + } + }, + "lambda": { + "title": "Lambda class descriptor", + "type": "object", + "properties": { + "declaringClass": { + "title": "The class in which the lambda class is defined", + "$ref": "#/$defs/className" + }, + "declaringMethod": { + "title": "The method in which the lambda class is defined", + "$ref": "#/$defs/method" + }, + "interfaces": { + "title": "Non-empty list of interfaces implemented by the lambda class", + "type": "array", + "items": { + "title": "Fully-qualified name of the interface implemented by the lambda class", + "$ref": "#/$defs/className" + }, + "minItems": 1 + } + }, + "required": [ + "declaringClass", + "interfaces" + ], + "additionalProperties": false + } + }, + "oneOf": [ + { + "required": [ + "proxy" + ] + }, + { + "required": [ + "lambda" + ] + } + ], + "additionalProperties": false + } + ] + }, + "method": { + "title": "Method descriptors used by GraalVM Native Image metadata files", + "type": "object", + "properties": { + "name": { + "title": "Method name that should be registered for this class", + "type": "string", + "pattern": "^[^.;\\[/]+$" + }, + "parameterTypes": { + "items": { + "title": "List of the method's parameter types", + "$ref": "#/$defs/typeName" + }, + "type": "array" + } + }, + "required": [ + "name", "parameterTypes" + ], + "additionalProperties": false + } + } +} diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml new file mode 100644 index 000000000000..72e273228a21 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging-oci-image/image-caches-pom.xml @@ -0,0 +1,22 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + docker.io/library/${project.artifactId}:build + + + + + + + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging/custom-layers-classpath-pom.xml b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging/custom-layers-classpath-pom.xml new file mode 100644 index 000000000000..ec4f10c356ce --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/examples/packaging/custom-layers-classpath-pom.xml @@ -0,0 +1,26 @@ + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + true + custom + + + + + com.example + layers-configuration + 1.0.0 + + + + + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc index e099dcef4ff3..0c492ca6b557 100644 --- a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/build-image.adoc @@ -494,6 +494,13 @@ The caches and the build workspace can be configured to use bind mounts instead include::example$packaging-oci-image/bind-caches-pom.xml[tags=caches] ---- +The build cache can also be stored in an image instead of a named volume, which allows the cache to be shared between hosts by pushing and pulling it from an image registry, as shown in the following example: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging-oci-image/image-caches-pom.xml[tags=caches] +---- + [[build-image.examples.docker]] diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/packaging.adoc b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/packaging.adoc index b0249a0c67d4..1da8d495310c 100644 --- a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/packaging.adoc +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/packaging.adoc @@ -83,13 +83,26 @@ include::example$packaging/disable-layers-pom.xml[tags=disable-layers] === Custom Layers Configuration Depending on your application, you may want to tune how layers are created and add new ones. -This can be done using a separate configuration file that should be registered as shown below: +This can be done using a configuration loaded from a file or from the classpath. + +To use a configuration file, register it as shown below: [source,xml,indent=0,subs="verbatim,attributes"] ---- include::example$packaging/custom-layers-pom.xml[tags=custom-layers] ---- +Alternatively, the configuration can be loaded from the classpath by specifying its name. +A matching file is expected at `META-INF/spring/layers/.xml` on the plugin's classpath. +The jar that contains the configuration can be added as a plugin dependency, as shown below: + +[source,xml,indent=0,subs="verbatim,attributes"] +---- +include::example$packaging/custom-layers-classpath-pom.xml[tags=custom-layers-classpath] +---- + +In the example above, `META-INF/spring/layers/custom.xml` is expected to be available on the plugin's classpath. + The configuration file describes how an archive can be separated into layers, and the order of those layers. The following example shows how the default ordering described above can be defined explicitly: diff --git a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/run.adoc b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/run.adoc index b06bf52fd78d..aa2eeb4b6a8c 100644 --- a/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/run.adoc +++ b/build-plugin/spring-boot-maven-plugin/src/docs/antora/modules/maven-plugin/pages/run.adoc @@ -26,8 +26,6 @@ include::example$running/devtools-pom.xml[tags=devtools] When `devtools` is running, it detects changes when you recompile your application and automatically refreshes it. This works for not only resources but code as well. -It also provides a LiveReload server so that it can automatically trigger a browser refresh whenever things change. - Devtools can also be configured to only refresh the browser whenever a static resource has changed (and ignore any change in the code). Just include the following property in your project: diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java index 4b9e7657906d..0bfe5a0fbf48 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/AbstractArchiveIntegrationTests.java @@ -38,6 +38,8 @@ import org.assertj.core.api.ListAssert; import org.jspecify.annotations.Nullable; +import org.springframework.lang.CheckReturnValue; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.contentOf; @@ -172,6 +174,7 @@ JarAssert doesNotHaveEntryWithNameStartingWith(String prefix) { return this; } + @CheckReturnValue ListAssert entryNamesInPath(String path) { List matches = new ArrayList<>(); withJarFile((jarFile) -> withEntries(jarFile, diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java index f7dd021d626f..14f247d8e2fb 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/JarIntegrationTests.java @@ -437,6 +437,27 @@ void whenJarIsRepackagedWithTheCustomLayers(MavenBuild mavenBuild) { }); } + @TestTemplate + void whenJarIsRepackagedWithTheCustomLayersFromClasspath(MavenBuild mavenBuild) { + mavenBuild.project("jar-layered-custom-name").execute((project) -> { + File repackaged = new File(project, "jar/target/jar-layered-0.0.1.BUILD-SNAPSHOT.jar"); + assertThat(jar(repackaged)).hasEntryWithNameStartingWith("BOOT-INF/classes/") + .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-release") + .hasEntryWithNameStartingWith("BOOT-INF/lib/jar-snapshot"); + try (JarFile jarFile = new JarFile(repackaged)) { + Map> layerIndex = readLayerIndex(jarFile); + assertThat(layerIndex.keySet()).containsExactly("my-dependencies-name", "snapshot-dependencies", + "configuration", "application"); + assertThat(layerIndex.get("application")) + .contains("BOOT-INF/lib/jar-release-0.0.1.RELEASE.jar", + "BOOT-INF/lib/jar-snapshot-0.0.1.BUILD-SNAPSHOT.jar", + "BOOT-INF/lib/jar-classifier-0.0.1-bravo.jar") + .doesNotContain("BOOT-INF/lib/jar-classifier-0.0.1-alpha.jar", + "BOOT-INF/lib/jar-layers-configuration-0.0.1.jar"); + } + }); + } + @TestTemplate void repackagedJarContainsClasspathIndex(MavenBuild mavenBuild) { mavenBuild.project("jar").execute((project) -> { diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java index bf9e6a214e37..f5d0bd15e53a 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/RunIntegrationTests.java @@ -95,8 +95,31 @@ void whenProfilesAreConfiguredTheyArePassedToTheApplication(MavenBuild mavenBuil } @TestTemplate + @Deprecated(since = "4.2.0", forRemoval = true) void whenUseTestClasspathIsEnabledTheApplicationHasTestDependenciesOnItsClasspath(MavenBuild mavenBuild) { mavenBuild.project("run-use-test-classpath") + .goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run") + .contains("useTestClasspath is deprecated, use testClasspath instead.")); + } + + @TestTemplate + void whenTestClasspathIsDependenciesTheApplicationHasTestDependenciesOnItsClasspath(MavenBuild mavenBuild) { + mavenBuild.project("run-test-classpath-dependencies") + .goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenTestClasspathIsAllTheApplicationHasTestAndClassesDependenciesOnItsClasspath(MavenBuild mavenBuild) { + mavenBuild.project("run-test-classpath-all") + .goals("spring-boot:run") + .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); + } + + @TestTemplate + void whenTestClasspathIsOffTheApplicationHasNeitherTestNorClassesDependenciesOnItsClasspath(MavenBuild mavenBuild) { + mavenBuild.project("run-test-classpath-off") .goals("spring-boot:run") .execute((project) -> assertThat(buildLog(project)).contains("I haz been run")); } diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java index 5e6b8efe1b04..acd0b6c215e1 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/java/org/springframework/boot/maven/StartStopIntegrationTests.java @@ -34,7 +34,17 @@ class StartStopIntegrationTests { @TestTemplate void startStopWaitsForApplicationToBeReadyAndThenRequestsShutdown(MavenBuild mavenBuild) { - mavenBuild.project("start-stop") + testStartStop(mavenBuild, "start-stop"); + + } + + @TestTemplate + void startStopWithTestClasspathWaitsForApplicationToBeReadyAndThenRequestsShutdown(MavenBuild mavenBuild) { + testStartStop(mavenBuild, "start-stop-test"); + } + + private void testStartStop(MavenBuild mavenBuild, String projectId) { + mavenBuild.project(projectId) .goals("verify") .execute((project) -> assertThat(buildLog(project)).contains("isReady: true") .contains("Shutdown requested")); diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-classifier/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-classifier/pom.xml new file mode 100644 index 000000000000..3dd0f2f83b44 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-classifier/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-classifier + 0.0.1 + jar + jar + Classifier Jar dependency + + + + maven-jar-plugin + + + alpha + package + + jar + + + alpha + + + + bravo + package + + jar + + + bravo + + + + + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/pom.xml new file mode 100644 index 000000000000..fe90fa206c28 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-layers-configuration + 0.0.1 + jar + jar + Layers configuration dependency + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml new file mode 100644 index 000000000000..a3e5e930dfaa --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-layers-configuration/src/main/resources/META-INF/spring/layers/custom.xml @@ -0,0 +1,26 @@ + + + + **/application*.* + + + + + + + + + *:*:*-SNAPSHOT + + + + + my-dependencies-name + snapshot-dependencies + configuration + application + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-release/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-release/pom.xml new file mode 100644 index 000000000000..a06fe545f187 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-release/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + jar + jar + Release Jar dependency + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-snapshot/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-snapshot/pom.xml new file mode 100644 index 000000000000..ab31e719baf5 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar-snapshot/pom.xml @@ -0,0 +1,11 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + jar + jar + Snapshot Jar dependency + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/pom.xml new file mode 100644 index 000000000000..ead499fa8b89 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + org.springframework.boot.maven.it + jar-layered + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + + repackage + + + + true + custom + + + + + + + org.springframework.boot.maven.it + jar-layers-configuration + 0.0.1 + + + + + + + + org.springframework.boot.maven.it + jar-snapshot + 0.0.1.BUILD-SNAPSHOT + + + org.springframework.boot.maven.it + jar-release + 0.0.1.RELEASE + + + org.springframework.boot.maven.it + jar-classifier + 0.0.1 + bravo + + + org.apache.logging.log4j + log4j-api + @log4j2.version@ + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..547d0cf01711 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-present 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/resources/application.yml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/jar/src/main/resources/application.yml new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/pom.xml new file mode 100644 index 000000000000..55914809a3f1 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom-name/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + org.springframework.boot.maven.it + aggregator + 0.0.1.BUILD-SNAPSHOT + pom + + UTF-8 + @java.version@ + @java.version@ + + + jar-classifier + jar-layers-configuration + jar-release + jar-snapshot + jar + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml index a7a3dc82f48b..13e533616163 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/jar-layered-custom/jar/src/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/layers/layers-4.2.xsd"> **/application*.* diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/pom.xml new file mode 100644 index 000000000000..4fedfc6053c9 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-test-classpath-all + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + all + + + + + + + org.springframework + spring-context + @spring-framework.version@ + test + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/src/test/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/src/test/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..19e05c51e088 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-all/src/test/java/org/test/SampleApplication.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-present 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + + Class appContext = null; + try { + appContext = Class.forName("org.springframework.context.ApplicationContext"); + } + catch (ClassNotFoundException e) { + throw new IllegalStateException("Test dependencies not added to classpath", e); + } + System.out.println("I haz been run"); + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/pom.xml new file mode 100644 index 000000000000..5bfc72a9110f --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-test-classpath-dependencies + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + dependencies + + + + + + + org.springframework + spring-context + @spring-framework.version@ + test + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/main/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..e2f44e459209 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-present 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + try { + Class.forName("org.springframework.context.ApplicationContext"); + } + catch (ClassNotFoundException e) { + throw new IllegalStateException("Test dependencies not added to classpath", e); + } + try { + Class.forName("org.test.SampleTest"); + throw new IllegalStateException("Test classes should not have been added to classpath"); + } + catch (ClassNotFoundException e) { + // OK + } + System.out.println("I haz been run"); + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/test/java/org/test/SampleTest.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/test/java/org/test/SampleTest.java new file mode 100644 index 000000000000..0fd0a1474a30 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-dependencies/src/test/java/org/test/SampleTest.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-present 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.test; + +public class SampleTest { + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/pom.xml new file mode 100644 index 000000000000..69d1b00a2fd4 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + org.springframework.boot.maven.it + run-test-classpath-off + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + off + + + + + + + org.springframework + spring-context + @spring-framework.version@ + test + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/main/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/main/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..725d88351caa --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/main/java/org/test/SampleApplication.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-present 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.test; + +public class SampleApplication { + + public static void main(String[] args) { + try { + Class.forName("org.springframework.context.ApplicationContext"); + throw new IllegalStateException("Test dependencies should not have been added to classpath"); + } + catch (ClassNotFoundException e) { + // OK + } + try { + Class.forName("org.test.SampleTest"); + throw new IllegalStateException("Test classes should not have been added to classpath"); + } + catch (ClassNotFoundException e) { + // OK + } + System.out.println("I haz been run"); + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/test/java/org/test/SampleTest.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/test/java/org/test/SampleTest.java new file mode 100644 index 000000000000..0fd0a1474a30 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/run-test-classpath-off/src/test/java/org/test/SampleTest.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-present 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.test; + +public class SampleTest { + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/pom.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/pom.xml new file mode 100644 index 000000000000..6613cb47a999 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + org.springframework.boot.maven.it + start-stop-test + 0.0.1.BUILD-SNAPSHOT + + UTF-8 + @java.version@ + @java.version@ + + + + + org.codehaus.mojo + build-helper-maven-plugin + @build-helper-maven-plugin.version@ + + + reserve-jmx-port + + reserve-network-port + + process-resources + + + jmx.port + + + + + + + @project.groupId@ + @project.artifactId@ + @project.version@ + + + pre-integration-test + + start + + + + post-integration-test + + stop + + + + + ${jmx.port} + all + + + + + diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdmin.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdmin.java new file mode 100644 index 000000000000..b56865a700c5 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdmin.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-present 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.test; + +final class SpringApplicationAdmin implements SpringApplicationAdminMXBean { + + boolean ready; + + boolean shutdownInvoked; + + @Override + public boolean isReady() { + System.out.println("isReady: " + this.ready); + return this.ready; + } + + @Override + public void shutdown() { + this.shutdownInvoked = true; + System.out.println("Shutdown requested"); + } + +} \ No newline at end of file diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdminMXBean.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdminMXBean.java new file mode 100644 index 000000000000..8c240fdaab64 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/main/java/org/test/SpringApplicationAdminMXBean.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-present 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.test; + +public interface SpringApplicationAdminMXBean { + + boolean isReady(); + + void shutdown(); + +} \ No newline at end of file diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/test/java/org/test/SampleApplication.java b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/test/java/org/test/SampleApplication.java new file mode 100644 index 000000000000..f2008431c359 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/start-stop-test/src/test/java/org/test/SampleApplication.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-present 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.test; + +import java.lang.management.ManagementFactory; + +import javax.management.MBeanServer; +import javax.management.ObjectName; + +/** + * This sample app simulates the JMX Mbean that is exposed by the Spring Boot application. + */ +public class SampleApplication { + + private static final Object lock = new Object(); + + public static void main(String[] args) throws Exception { + MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + ObjectName name = new ObjectName( + "org.springframework.boot:type=Admin,name=SpringApplication"); + SpringApplicationAdmin mbean = new SpringApplicationAdmin(); + mbs.registerMBean(mbean, name); + + // Flag the app as ready + mbean.ready = true; + + int waitAttempts = 0; + while (!mbean.shutdownInvoked) { + if (waitAttempts > 30) { + throw new IllegalStateException( + "Shutdown should have been invoked by now"); + } + synchronized (lock) { + lock.wait(250); + } + waitAttempts++; + } + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml index a7a3dc82f48b..13e533616163 100644 --- a/build-plugin/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml +++ b/build-plugin/spring-boot-maven-plugin/src/intTest/projects/war-layered-custom/war/src/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/layers/layers-4.2.xsd"> **/application*.* diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java index e77ac55ea88e..d25715a938f2 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractPackagerMojo.java @@ -18,6 +18,8 @@ import java.io.File; import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -32,6 +34,7 @@ import org.apache.maven.execution.MavenSession; import org.apache.maven.model.Dependency; import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.descriptor.PluginDescriptor; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProjectHelper; @@ -79,6 +82,14 @@ public abstract class AbstractPackagerMojo extends AbstractDependencyFilterMojo @SuppressWarnings("NullAway.Init") protected MavenSession session; + /** + * The plugin descriptor. + * @since 4.1.0 + */ + @Parameter(defaultValue = "${plugin}", readonly = true, required = true) + @SuppressWarnings("NullAway.Init") + private PluginDescriptor pluginDescriptor; + /** * Maven project helper utils. * @since 1.0.0 @@ -175,8 +186,7 @@ protected

P getConfiguredPackager(Supplier

supplier) { packager.setLayout(layout.layout()); } if (this.layers.isEnabled()) { - packager.setLayers((this.layers.getConfiguration() != null) - ? getCustomLayers(this.layers.getConfiguration()) : IMPLICIT_LAYERS); + packager.setLayers(loadLayersConfiguration()); } packager.setIncludeRelevantJarModeJars(getIncludeRelevantJarModeJars()); return packager; @@ -186,25 +196,48 @@ private boolean getIncludeRelevantJarModeJars() { return this.includeTools; } - private CustomLayers getCustomLayers(File configuration) { + private org.springframework.boot.loader.tools.Layers loadLayersConfiguration() { + File configuration = this.layers.getConfiguration(); + if (configuration != null) { + return getCustomLayers(configuration.getAbsolutePath(), () -> new FileInputStream(configuration)); + } + String configurationName = this.layers.getConfigurationName(); + if (configurationName != null) { + String location = "META-INF/spring/layers/%s.xml".formatted(configurationName); + return getCustomLayers(location, () -> loadLayersConfigurationFromClasspath(configurationName, location)); + } + return IMPLICIT_LAYERS; + } + + private InputStream loadLayersConfigurationFromClasspath(String name, String location) { + InputStream in = this.pluginDescriptor.getClassRealm().getResourceAsStream(location); + if (in == null) { + throw new IllegalStateException( + "Failed to load layers configuration with name '%s': '%s' not found".formatted(name, location)); + } + return in; + } + + private CustomLayers getCustomLayers(String source, InputStreamSource inputStreamSource) { try { - Document document = getDocumentIfAvailable(configuration); + Document document = getDocumentIfAvailable(inputStreamSource); return new CustomLayersProvider().getLayers(document); } catch (Exception ex) { - throw new IllegalStateException( - "Failed to process custom layers configuration " + configuration.getAbsolutePath(), ex); + throw new IllegalStateException("Failed to process custom layers configuration " + source, ex); } } - private Document getDocumentIfAvailable(File xmlFile) throws Exception { - InputSource inputSource = new InputSource(new FileInputStream(xmlFile)); - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); - factory.setNamespaceAware(true); - DocumentBuilder builder = factory.newDocumentBuilder(); - return builder.parse(inputSource); + private Document getDocumentIfAvailable(InputStreamSource source) throws Exception { + try (InputStream in = source.getInputStream()) { + InputSource inputSource = new InputSource(in); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setNamespaceAware(true); + DocumentBuilder builder = factory.newDocumentBuilder(); + return builder.parse(inputSource); + } } /** @@ -314,4 +347,11 @@ public Layout layout() { } + @FunctionalInterface + private interface InputStreamSource { + + InputStream getInputStream() throws IOException; + + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java index 08feb075567f..cb1ce5f26461 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/AbstractRunMojo.java @@ -205,6 +205,16 @@ public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { @SuppressWarnings("NullAway.Init") private File classesDirectory; + /** + * Directory containing the test classes and resource files that can be included when + * running the application. + * + * @since 4.2.0 + */ + @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true) + @SuppressWarnings("NullAway.Init") + private File testClassesDirectory; + /** * Skip the execution. * @@ -241,10 +251,19 @@ private String determineMainClass() throws MojoExecutionException { * @since 3.1.0 */ protected List getClassesDirectories() { - return List.of(this.classesDirectory); + List classesDirectories = new ArrayList<>(); + if (testClasspath() == TestClasspath.ALL) { + classesDirectories.add(this.testClassesDirectory); + } + classesDirectories.add(this.classesDirectory); + return Collections.unmodifiableList(classesDirectories); } - protected abstract boolean isUseTestClasspath(); + /** + * Return the {@link TestClasspath} strategy to use. + * @return the test classpath strategy + */ + protected abstract TestClasspath testClasspath(); private void run(String startClassName) throws MojoExecutionException, MojoFailureException { List args = new ArrayList<>(); @@ -416,7 +435,9 @@ private void addProjectClasses(List urls) throws MalformedURLException { } private void addDependencies(List urls) throws MalformedURLException, MojoExecutionException { - Set artifacts = (isUseTestClasspath()) ? filterDependencies(this.project.getArtifacts()) + TestClasspath testClasspath = testClasspath(); + Set artifacts = ((testClasspath == TestClasspath.ALL || testClasspath == TestClasspath.DEPENDENCIES)) + ? filterDependencies(this.project.getArtifacts()) : filterDependencies(this.project.getArtifacts(), new ExcludeTestScopeArtifactFilter()); for (Artifact artifact : artifacts) { if (artifact.getFile() != null) { @@ -432,4 +453,27 @@ private void logArguments(String name, String[] args) { } } + /** + * Strategies to handle the test classpath when running an application. + */ + protected enum TestClasspath { + + /** + * Do not include the test classpath at all. + */ + OFF, + + /** + * Include only dependencies with test scope. + */ + DEPENDENCIES, + + /** + * Include the full test classpath (test classes and dependencies with test + * scope). + */ + ALL + + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java index 98d23db744bd..32a05ea6d44a 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/CacheInfo.java @@ -19,6 +19,8 @@ import org.jspecify.annotations.Nullable; import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.maven.LocalCacheInfo.BindCacheInfo; +import org.springframework.boot.maven.LocalCacheInfo.VolumeCacheInfo; import org.springframework.util.Assert; /** @@ -52,6 +54,13 @@ public void setBind(BindCacheInfo info) { this.cache = Cache.bind(source); } + public void setImage(ImageCacheInfo info) { + Assert.state(this.cache == null, "Each image building cache can be configured only once"); + String name = info.getName(); + Assert.state(name != null, "'name' must not be null"); + this.cache = Cache.image(name); + } + @Nullable Cache asCache() { return this.cache; } @@ -68,17 +77,23 @@ static CacheInfo fromBind(BindCacheInfo cacheInfo) { return new CacheInfo(Cache.bind(source)); } + static CacheInfo fromImage(ImageCacheInfo cacheInfo) { + String name = cacheInfo.getName(); + Assert.state(name != null, "'name' must not be null"); + return new CacheInfo(Cache.image(name)); + } + /** - * Encapsulates configuration of an image building cache stored in a volume. + * Encapsulates configuration of an image building cache stored in an image. */ - public static class VolumeCacheInfo { + public static class ImageCacheInfo { private @Nullable String name; - public VolumeCacheInfo() { + public ImageCacheInfo() { } - VolumeCacheInfo(String name) { + ImageCacheInfo(String name) { this.name = name; } @@ -92,28 +107,4 @@ void setName(@Nullable String name) { } - /** - * Encapsulates configuration of an image building cache stored in a bind mount. - */ - public static class BindCacheInfo { - - private @Nullable String source; - - public BindCacheInfo() { - } - - BindCacheInfo(String name) { - this.source = name; - } - - public @Nullable String getSource() { - return this.source; - } - - void setSource(@Nullable String source) { - this.source = source; - } - - } - } diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java index 42e7f7ca4cdb..3b359cc4a892 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Image.java @@ -26,6 +26,7 @@ import org.springframework.boot.buildpack.platform.build.BuildRequest; import org.springframework.boot.buildpack.platform.build.BuildpackReference; import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.buildpack.platform.build.LocalCache; import org.springframework.boot.buildpack.platform.build.PullPolicy; import org.springframework.boot.buildpack.platform.docker.type.Binding; import org.springframework.boot.buildpack.platform.docker.type.ImageName; @@ -74,11 +75,11 @@ public class Image { @Nullable List tags; - @Nullable CacheInfo buildWorkspace; + @Nullable LocalCacheInfo buildWorkspace; @Nullable CacheInfo buildCache; - @Nullable CacheInfo launchCache; + @Nullable LocalCacheInfo launchCache; @Nullable String createdDate; @@ -284,7 +285,7 @@ private BuildRequest customize(BuildRequest request) { request = request.withTags(this.tags.stream().map(ImageReference::of).toList()); } if (this.buildWorkspace != null) { - Cache cache = this.buildWorkspace.asCache(); + LocalCache cache = this.buildWorkspace.asCache(); Assert.state(cache != null, "'cache' must not be null"); request = request.withBuildWorkspace(cache); } @@ -294,7 +295,7 @@ private BuildRequest customize(BuildRequest request) { request = request.withBuildCache(cache); } if (this.launchCache != null) { - Cache cache = this.launchCache.asCache(); + LocalCache cache = this.launchCache.asCache(); Assert.state(cache != null, "'cache' must not be null"); request = request.withLaunchCache(cache); } diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java index 9ac86ff3b1a1..20d33f585d7f 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/Layers.java @@ -32,6 +32,8 @@ public class Layers { private @Nullable File configuration; + private @Nullable String configurationName; + /** * Whether a {@code layers.idx} file should be added to the jar. * @return true if a {@code layers.idx} file should be added. @@ -54,4 +56,17 @@ public void setConfiguration(@Nullable File configuration) { this.configuration = configuration; } + /** + * The name of the layers configuration to load from the classpath. A matching file is + * expected at {@code META-INF/spring/layers/.xml} + * @return the layers configuration name to load from the classpath + */ + public @Nullable String getConfigurationName() { + return this.configurationName; + } + + public void setConfigurationName(@Nullable String configurationName) { + this.configurationName = configurationName; + } + } diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LocalCacheInfo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LocalCacheInfo.java new file mode 100644 index 000000000000..8d088f246753 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/LocalCacheInfo.java @@ -0,0 +1,121 @@ +/* + * Copyright 2012-present 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.maven; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.buildpack.platform.build.Cache; +import org.springframework.boot.buildpack.platform.build.LocalCache; +import org.springframework.util.Assert; + +/** + * Encapsulates configuration of a local image building cache. + * + * @author Scott Frederick + * @author Stephane Nicoll + * @since 4.2.0 + */ +public class LocalCacheInfo { + + private @Nullable LocalCache cache; + + public LocalCacheInfo() { + } + + LocalCacheInfo(LocalCache cache) { + this.cache = cache; + } + + public void setVolume(VolumeCacheInfo info) { + Assert.state(this.cache == null, "Each image building cache can be configured only once"); + String name = info.getName(); + Assert.state(name != null, "'name' must not be null"); + this.cache = Cache.volume(name); + } + + public void setBind(BindCacheInfo info) { + Assert.state(this.cache == null, "Each image building cache can be configured only once"); + String source = info.getSource(); + Assert.state(source != null, "'source' must not be null"); + this.cache = Cache.bind(source); + } + + @Nullable LocalCache asCache() { + return this.cache; + } + + static LocalCacheInfo fromVolume(VolumeCacheInfo cacheInfo) { + String name = cacheInfo.getName(); + Assert.state(name != null, "'name' must not be null"); + return new LocalCacheInfo(Cache.volume(name)); + } + + static LocalCacheInfo fromBind(BindCacheInfo cacheInfo) { + String source = cacheInfo.getSource(); + Assert.state(source != null, "'source' must not be null"); + return new LocalCacheInfo(Cache.bind(source)); + } + + /** + * Encapsulates configuration of an image building cache stored in a volume. + */ + public static class VolumeCacheInfo { + + private @Nullable String name; + + public VolumeCacheInfo() { + } + + VolumeCacheInfo(String name) { + this.name = name; + } + + public @Nullable String getName() { + return this.name; + } + + void setName(@Nullable String name) { + this.name = name; + } + + } + + /** + * Encapsulates configuration of an image building cache stored in a bind mount. + */ + public static class BindCacheInfo { + + private @Nullable String source; + + public BindCacheInfo() { + } + + BindCacheInfo(String name) { + this.source = name; + } + + public @Nullable String getSource() { + return this.source; + } + + void setSource(@Nullable String source) { + this.source = source; + } + + } + +} diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java index 8992ed3d55f6..ee6332ca0760 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/RunMojo.java @@ -18,6 +18,7 @@ import java.io.File; import java.util.List; +import java.util.Locale; import java.util.Map; import javax.inject.Inject; @@ -30,6 +31,7 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.toolchain.ToolchainManager; +import org.jspecify.annotations.Nullable; import org.springframework.boot.loader.tools.RunProcess; @@ -57,9 +59,24 @@ public class RunMojo extends AbstractRunMojo { /** * Flag to include the test classpath when running. * @since 1.3.0 + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of {@code testClasspath} */ - @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") - private boolean useTestClasspath; + @Parameter(property = "spring-boot.run.useTestClasspath") + @Deprecated(since = "4.2.0", forRemoval = true) + private @Nullable Boolean useTestClasspath; + + /** + * Strategy to determine how the test classpath should be included when running the + * application. Available values are {@code off} to not include the test classpath at + * all, {@code dependencies} to include only dependencies with test scope, and + * {@code all} to include the full test classpath (test classes and dependencies with + * test scope). + * @since 4.2.0 + */ + @Parameter(property = "spring-boot.run.testClasspath", defaultValue = "off") + private String testClasspath = "off"; + + private @Nullable TestClasspath resolvedTestClasspath; @Inject public RunMojo(ToolchainManager toolchainManager) { @@ -85,8 +102,17 @@ protected void run(JavaProcessExecutor processExecutor, File workingDirectory, L } @Override - protected boolean isUseTestClasspath() { - return this.useTestClasspath; + protected TestClasspath testClasspath() { + if (this.resolvedTestClasspath == null) { + if (this.useTestClasspath != null) { + getLog().warn("useTestClasspath is deprecated, use testClasspath instead."); + this.resolvedTestClasspath = (this.useTestClasspath) ? TestClasspath.DEPENDENCIES : TestClasspath.OFF; + } + else { + this.resolvedTestClasspath = TestClasspath.valueOf(this.testClasspath.toUpperCase(Locale.ROOT)); + } + } + return this.resolvedTestClasspath; } private static final class RunProcessKiller implements Runnable { diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java index 728e663546c0..39fd8a6605f0 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/StartMojo.java @@ -21,6 +21,7 @@ import java.net.ConnectException; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.Callable; @@ -91,9 +92,24 @@ public class StartMojo extends AbstractRunMojo { /** * Flag to include the test classpath when running. + * @deprecated since 4.2.0 for removal in 4.4.0 in favor of {@code testClasspath} */ - @Parameter(property = "spring-boot.run.useTestClasspath", defaultValue = "false") - private boolean useTestClasspath; + @Parameter(property = "spring-boot.run.useTestClasspath") + @Deprecated(since = "4.2.0", forRemoval = true) + private @Nullable Boolean useTestClasspath; + + /** + * Strategy to determine how the test classpath should be included when running the + * application. Available values are {@code off} to not include the test classpath at + * all, {@code dependencies} to include only dependencies with test scope, and + * {@code all} to include the full test classpath (test classes and dependencies with + * test scope). + * @since 4.2.0 + */ + @Parameter(property = "spring-boot.run.testClasspath", defaultValue = "off") + private String testClasspath = "off"; + + private @Nullable TestClasspath resolvedTestClasspath; @Inject public StartMojo(ToolchainManager toolchainManager) { @@ -205,8 +221,17 @@ public T execute(long wait, int maxAttempts, Callable<@Nullable T> callback) } @Override - protected boolean isUseTestClasspath() { - return this.useTestClasspath; + protected TestClasspath testClasspath() { + if (this.resolvedTestClasspath == null) { + if (this.useTestClasspath != null) { + getLog().warn("useTestClasspath is deprecated, use testClasspath instead."); + this.resolvedTestClasspath = (this.useTestClasspath) ? TestClasspath.DEPENDENCIES : TestClasspath.OFF; + } + else { + this.resolvedTestClasspath = TestClasspath.valueOf(this.testClasspath.toUpperCase(Locale.ROOT)); + } + } + return this.resolvedTestClasspath; } private class CreateJmxConnector implements Callable<@Nullable JMXConnector> { diff --git a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java index 4be0c6185e6a..9ece33a41c63 100644 --- a/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java +++ b/build-plugin/spring-boot-maven-plugin/src/main/java/org/springframework/boot/maven/TestRunMojo.java @@ -17,7 +17,6 @@ package org.springframework.boot.maven; import java.io.File; -import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -57,29 +56,14 @@ public class TestRunMojo extends AbstractRunMojo { @Parameter(property = "spring-boot.test-run.optimizedLaunch", defaultValue = "true") private boolean optimizedLaunch; - /** - * Directory containing the test classes and resource files that should be used to run - * the application. - */ - @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true) - @SuppressWarnings("NullAway.Init") - private File testClassesDirectory; - @Inject public TestRunMojo(ToolchainManager toolchainManager) { super(toolchainManager); } @Override - protected List getClassesDirectories() { - ArrayList classesDirectories = new ArrayList<>(super.getClassesDirectories()); - classesDirectories.add(0, this.testClassesDirectory); - return classesDirectories; - } - - @Override - protected boolean isUseTestClasspath() { - return true; + protected TestClasspath testClasspath() { + return TestClasspath.ALL; } @Override diff --git a/build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml b/build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml new file mode 100644 index 000000000000..c94067c8abee --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/main/resources/META-INF/spring/layers/default.xml @@ -0,0 +1,26 @@ + + + + org/springframework/boot/loader/** + + + + + + + + + *:*:*SNAPSHOT + + + + + dependencies + spring-boot-loader + snapshot-dependencies + application + + diff --git a/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.1.xsd b/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.1.xsd new file mode 100644 index 000000000000..20219b9bd8b1 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.1.xsd @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.2.xsd b/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.2.xsd new file mode 100644 index 000000000000..20219b9bd8b1 --- /dev/null +++ b/build-plugin/spring-boot-maven-plugin/src/main/xsd/layers-4.2.xsd @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java b/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java index a6a2b4d55bac..2e0c7b7a1802 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java +++ b/build-plugin/spring-boot-maven-plugin/src/test/java/org/springframework/boot/maven/ImageTests.java @@ -36,8 +36,9 @@ import org.springframework.boot.buildpack.platform.docker.type.ImageReference; import org.springframework.boot.buildpack.platform.io.Owner; import org.springframework.boot.buildpack.platform.io.TarArchive; -import org.springframework.boot.maven.CacheInfo.BindCacheInfo; -import org.springframework.boot.maven.CacheInfo.VolumeCacheInfo; +import org.springframework.boot.maven.CacheInfo.ImageCacheInfo; +import org.springframework.boot.maven.LocalCacheInfo.BindCacheInfo; +import org.springframework.boot.maven.LocalCacheInfo.VolumeCacheInfo; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.entry; @@ -200,7 +201,7 @@ void getBuildRequestWhenHasTagsUsesTags() { @Test void getBuildRequestWhenHasBuildWorkspaceVolumeUsesWorkspace() { Image image = new Image(); - image.buildWorkspace = CacheInfo.fromVolume(new VolumeCacheInfo("build-work-vol")); + image.buildWorkspace = LocalCacheInfo.fromVolume(new VolumeCacheInfo("build-work-vol")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getBuildWorkspace()).isEqualTo(Cache.volume("build-work-vol")); } @@ -216,7 +217,7 @@ void getBuildRequestWhenHasBuildCacheVolumeUsesCache() { @Test void getBuildRequestWhenHasLaunchCacheVolumeUsesCache() { Image image = new Image(); - image.launchCache = CacheInfo.fromVolume(new VolumeCacheInfo("launch-cache-vol")); + image.launchCache = LocalCacheInfo.fromVolume(new VolumeCacheInfo("launch-cache-vol")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getLaunchCache()).isEqualTo(Cache.volume("launch-cache-vol")); } @@ -224,7 +225,7 @@ void getBuildRequestWhenHasLaunchCacheVolumeUsesCache() { @Test void getBuildRequestWhenHasBuildWorkspaceBindUsesWorkspace() { Image image = new Image(); - image.buildWorkspace = CacheInfo.fromBind(new BindCacheInfo("build-work-dir")); + image.buildWorkspace = LocalCacheInfo.fromBind(new BindCacheInfo("build-work-dir")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getBuildWorkspace()).isEqualTo(Cache.bind("build-work-dir")); } @@ -240,11 +241,19 @@ void getBuildRequestWhenHasBuildCacheBindUsesCache() { @Test void getBuildRequestWhenHasLaunchCacheBindUsesCache() { Image image = new Image(); - image.launchCache = CacheInfo.fromBind(new BindCacheInfo("launch-cache-dir")); + image.launchCache = LocalCacheInfo.fromBind(new BindCacheInfo("launch-cache-dir")); BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); assertThat(request.getLaunchCache()).isEqualTo(Cache.bind("launch-cache-dir")); } + @Test + void getBuildRequestWhenHasBuildCacheImageUsesCache() { + Image image = new Image(); + image.buildCache = CacheInfo.fromImage(new ImageCacheInfo("build-cache-image")); + BuildRequest request = image.getBuildRequest(createArtifact(), mockApplicationContent()); + assertThat(request.getBuildCache()).isEqualTo(Cache.image("build-cache-image")); + } + @Test void getBuildRequestWhenHasCreatedDateUsesCreatedDate() { Image image = new Image(); diff --git a/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml b/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml index 32808c3122fa..563e476cf362 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml +++ b/build-plugin/spring-boot-maven-plugin/src/test/resources/dependencies-layer-no-filter.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.2.xsd"> diff --git a/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml b/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml index b91c1f65d8e8..be1771b98d84 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml +++ b/build-plugin/spring-boot-maven-plugin/src/test/resources/layers.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.2.xsd"> META-INF/resources/** diff --git a/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml b/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml index 2669650cfc76..5f71408c5613 100644 --- a/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml +++ b/build-plugin/spring-boot-maven-plugin/src/test/resources/resource-layer-no-filter.xml @@ -1,7 +1,7 @@ + https://www.springframework.org/schema/boot/layers/layers-4.2.xsd"> diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index ae81960f3284..06abc6e2f5d6 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -46,6 +46,7 @@ dependencies { implementation(platform("tools.jackson:jackson-bom:${jacksonVersion}")) implementation(platform("org.springframework:spring-framework-bom:${springFrameworkVersion}")) implementation("com.github.node-gradle:gradle-node-plugin:7.1.0") + implementation("com.google.protobuf:protobuf-gradle-plugin:${protobufGradlePluginVersion}") implementation("com.gradle:develocity-gradle-plugin:4.2.2") implementation("com.tngtech.archunit:archunit:1.4.1") implementation("commons-codec:commons-codec:${commonsCodecVersion}") diff --git a/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java index 7c7a47e5051d..849968738450 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/ConventionsPlugin.java @@ -43,14 +43,17 @@ public class ConventionsPlugin implements Plugin { @Override public void apply(Project project) { + SystemRequirementsExtension systemRequirements = project.getExtensions() + .create("systemRequirements", SystemRequirementsExtension.class); new NoHttpConventions().apply(project); - new JavaConventions().apply(project); + new JavaConventions(systemRequirements).apply(project); new MavenPublishingConventions().apply(project); new AntoraConventions().apply(project); new KotlinConventions().apply(project); new WarConventions().apply(project); - new EclipseConventions().apply(project); + new EclipseConventions(systemRequirements).apply(project); new TestFixturesConventions().apply(project); + new ProtobufConventions().apply(project); RepositoryTransformersExtension.apply(project); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/EclipseConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/EclipseConventions.java index 7c6a13a9e36c..fd1c33f219bc 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/EclipseConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/EclipseConventions.java @@ -38,6 +38,12 @@ */ class EclipseConventions { + private final SystemRequirementsExtension systemRequirements; + + EclipseConventions(SystemRequirementsExtension systemRequirements) { + this.systemRequirements = systemRequirements; + } + void apply(Project project) { project.getPlugins().withType(EclipsePlugin.class, (eclipse) -> configure(project, eclipse)); project.afterEvaluate(this::setJavaRuntimeName); @@ -97,7 +103,7 @@ private void setJavaRuntimeName(Project project) { EclipseModel model = project.getExtensions().findByType(EclipseModel.class); EclipseJdt jdt = (model != null) ? model.getJdt() : null; if (jdt != null) { - model.getJdt().setJavaRuntimeName("JavaSE-" + JavaConventions.RUNTIME_JAVA_VERSION); + model.getJdt().setJavaRuntimeName("JavaSE-" + this.systemRequirements.getJava().getVersion()); } } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java index c416bf613203..4c552e876aa8 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/JavaConventions.java @@ -42,6 +42,7 @@ import org.gradle.api.artifacts.ConfigurationContainer; import org.gradle.api.artifacts.Dependency; import org.gradle.api.artifacts.DependencySet; +import org.gradle.api.file.FileTreeElement; import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; @@ -50,6 +51,7 @@ import org.gradle.api.plugins.quality.CheckstylePlugin; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.SourceTask; import org.gradle.api.tasks.TaskProvider; import org.gradle.api.tasks.bundling.Jar; import org.gradle.api.tasks.compile.JavaCompile; @@ -137,6 +139,12 @@ class JavaConventions { public static final int RUNTIME_JAVA_VERSION = 17; + private final SystemRequirementsExtension systemRequirements; + + JavaConventions(SystemRequirementsExtension systemRequirements) { + this.systemRequirements = systemRequirements; + } + void apply(Project project) { project.getPlugins().withType(JavaBasePlugin.class, (java) -> { project.getPlugins().apply(TestFailuresPlugin.class); @@ -175,7 +183,7 @@ private void configureJarManifestConventions(Project project) { Map attributes = new TreeMap<>(); attributes.put("Automatic-Module-Name", project.getName().replace("-", ".")); // Build-Jdk-Spec is used by buildpacks to pick the JRE to install - attributes.put("Build-Jdk-Spec", RUNTIME_JAVA_VERSION); + attributes.put("Build-Jdk-Spec", this.systemRequirements.getJava().getVersion()); attributes.put("Built-By", "Spring"); attributes.put("Implementation-Title", determineImplementationTitle(project, sourceJarTaskNames, javadocJarTaskNames, jar)); @@ -292,6 +300,17 @@ private void configureSpringJavaFormat(Project project) { .add(project.getDependencies().create("com.puppycrawl.tools:checkstyle:" + checkstyle.getToolVersion())); checkstyleDependencies .add(project.getDependencies().create("io.spring.javaformat:spring-javaformat-checkstyle:" + version)); + project.getTasks().withType(CheckFormat.class, this::excludeGeneratedSources); + project.getTasks().withType(Checkstyle.class, this::excludeGeneratedSources); + } + + private SourceTask excludeGeneratedSources(SourceTask task) { + return task.exclude(this::isGeneratedSource); + } + + private boolean isGeneratedSource(FileTreeElement candidate) { + String path = StringUtils.cleanPath(candidate.getFile().getPath()); + return path.contains("/generated/sources/") || path.contains("/generated-source/"); } private void configureDependencyManagement(Project project) { diff --git a/buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java b/buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java new file mode 100644 index 000000000000..8b8cff22caf5 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/ProtobufConventions.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present 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 com.google.protobuf.gradle.ProtobufExtension; +import com.google.protobuf.gradle.ProtobufPlugin; +import org.gradle.api.Project; + +/** + * Conventions that are applied in the presence of the {@link ProtobufPlugin} plugin. + * + * @author Eric Haag + */ +class ProtobufConventions { + + void apply(Project project) { + project.getPlugins().withId("com.google.protobuf", (plugin) -> { + ProtobufExtension protobuf = project.getExtensions().getByType(ProtobufExtension.class); + removeUnusedMachineSpecificConfiguration(protobuf); + }); + } + + // See: https://github.com/google/protobuf-gradle-plugin/issues/785 + private void removeUnusedMachineSpecificConfiguration(ProtobufExtension protobuf) { + protobuf.getJavaExecutablePath().convention(""); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java new file mode 100644 index 000000000000..d23f930673b0 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/boot/build/SystemRequirementsExtension.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-present 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 javax.inject.Inject; + +import org.gradle.api.Action; +import org.gradle.api.model.ObjectFactory; + +/** + * DSL extension for configuring a project's system requirements. + * + * @author Andy Wilkinson + */ +public class SystemRequirementsExtension { + + private final JavaSpec javaSpec; + + @Inject + public SystemRequirementsExtension(ObjectFactory objects) { + this.javaSpec = objects.newInstance(JavaSpec.class); + } + + public void java(Action action) { + action.execute(this.javaSpec); + } + + public JavaSpec getJava() { + return this.javaSpec; + } + + public abstract static class JavaSpec { + + private int version = 17; + + public int getVersion() { + return this.version; + } + + public void setVersion(int version) { + this.version = version; + } + + } + +} diff --git a/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java b/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java index 39ee2273c9d6..9b1a6e59c4ce 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/antora/AntoraAsciidocAttributes.java @@ -161,6 +161,9 @@ private void addVersionAttributes(Map attributes, Map ArchitectureRules.configurationProperties(ArchitectureCheckAnnotation .classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES)))); + getRules().addAll(whenMainSources(() -> ArchitectureRules.configurationProperties(ArchitectureCheckAnnotation + .classFor(getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_SOURCE)))); getRules().addAll(whenMainSources( () -> ArchitectureRules.configurationPropertiesBinding(ArchitectureCheckAnnotation.classFor( getAnnotationClasses().get(), ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_BINDING)))); getRules().addAll(whenMainSources( () -> ArchitectureRules.configurationPropertiesDeprecation(ArchitectureCheckAnnotation.classFor( getAnnotationClasses().get(), ArchitectureCheckAnnotation.DEPRECATED_CONFIGURATION_PROPERTY)))); + getRules().addAll(whenMainSources(() -> Collections.singletonList( + ArchitectureRules.allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue()))); getRuleDescriptions().set(getRules().map(this::asDescriptions)); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckAnnotation.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckAnnotation.java index 991bdaa7c27a..bd7ff6d6ea75 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckAnnotation.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureCheckAnnotation.java @@ -42,6 +42,11 @@ public enum ArchitectureCheckAnnotation { */ CONFIGURATION_PROPERTIES, + /** + * Configuration properties source. + */ + CONFIGURATION_PROPERTIES_SOURCE, + /** * Deprecated configuration property. */ @@ -56,6 +61,8 @@ public enum ArchitectureCheckAnnotation { "org.springframework.boot.autoconfigure.condition.ConditionalOnClass", CONDITIONAL_ON_MISSING_BEAN.name(), "org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean", CONFIGURATION_PROPERTIES.name(), "org.springframework.boot.context.properties.ConfigurationProperties", + CONFIGURATION_PROPERTIES_SOURCE.name(), + "org.springframework.boot.context.properties.ConfigurationPropertiesSource", DEPRECATED_CONFIGURATION_PROPERTY.name(), "org.springframework.boot.context.properties.DeprecatedConfigurationProperty", CONFIGURATION_PROPERTIES_BINDING.name(), diff --git a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java index b96a600e43f6..02d3aaace8f6 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/architecture/ArchitectureRules.java @@ -37,7 +37,10 @@ import com.tngtech.archunit.core.domain.JavaClass.Predicates; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.domain.JavaConstructor; +import com.tngtech.archunit.core.domain.JavaConstructorCall; import com.tngtech.archunit.core.domain.JavaField; +import com.tngtech.archunit.core.domain.JavaFieldAccess; +import com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType; import com.tngtech.archunit.core.domain.JavaMember; import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaModifier; @@ -63,6 +66,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Role; +import org.springframework.lang.CheckReturnValue; import org.springframework.util.ResourceUtils; /** @@ -75,6 +79,8 @@ * @author Phillip Webb * @author Ngoc Nhan * @author Moritz Halbritter + * @author Stefano Cordio + * @author Venkata Naga Sai Srikanth Gollapudi */ final class ArchitectureRules { @@ -124,7 +130,11 @@ static List conditionalOnMissingBean(String annotationClass) { static List configurationProperties(String annotationClass) { return List.of(classLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass), - methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass)); + methodLevelConfigurationPropertiesShouldNotSpecifyOnlyPrefixAttribute(annotationClass), + configurationPropertyOfTypeShouldUseImplementation(annotationClass, "java.util.Set", + List.of("java.util.LinkedHashSet")), + configurationPropertyOfTypeShouldUseImplementation(annotationClass, "java.util.Map", + List.of("java.util.LinkedHashMap", "java.util.EnumMap"))); } static List configurationPropertiesBinding(String annotationClass) { @@ -158,13 +168,28 @@ private static ArchRule allBeanMethodsShouldNotHaveConditionalOnClassAnnotation( .allowEmptyShould(true); } - private static ArchRule allPackagesShouldBeFreeOfTangles() { - return SlicesRuleDefinition.slices() - .matching("(**)") + static ArchRule allCustomAssertionMethodsNotReturningSelfShouldBeAnnotatedWithCheckReturnValue() { + return ArchRuleDefinition.methods() + .that() + .areDeclaredInClassesThat() + .implement("org.assertj.core.api.Assert") + .and() + .arePublic() + .and() + .doNotHaveModifier(JavaModifier.BRIDGE) + .and(doNotReturnSelfType()) .should() - .beFreeOfCycles() - .ignoreDependency("org.springframework.boot.env.EnvironmentPostProcessor", - "org.springframework.boot.SpringApplication"); + .beAnnotatedWith(CheckReturnValue.class) + .allowEmptyShould(true); + } + + private static DescribedPredicate doNotReturnSelfType() { + return DescribedPredicate.describe("do not return self type", + (method) -> !method.getRawReturnType().equals(method.getOwner())); + } + + private static ArchRule allPackagesShouldBeFreeOfTangles() { + return SlicesRuleDefinition.slices().matching("(**)").should().beFreeOfCycles(); } private static ArchRule allBeanPostProcessorBeanMethodsShouldBeStaticAndNotCausePrematureInitialization() { @@ -406,6 +431,54 @@ private static ArchRule allDeprecatedConfigurationPropertiesShouldIncludeSince(S .allowEmptyShould(true); } + private static ArchRule configurationPropertyOfTypeShouldUseImplementation(String annotationClass, + String propertyType, List allowedImplementations) { + return ArchRuleDefinition.classes() + .that() + .areAnnotatedWith(annotationClass) + .or(areNestedInClassAnnotatedWith(annotationClass)) + .should(useAllowedImplementationForProperties(propertyType, allowedImplementations)) + .allowEmptyShould(true); + } + + private static ArchCondition useAllowedImplementationForProperties(String propertyType, + List allowedImplementations) { + return check( + "use %s for properties of type %s".formatted(String.join(" or ", allowedImplementations), propertyType), + (javaClass, events) -> javaClass.getFields() + .stream() + .filter((field) -> propertyType.equals(field.getRawType().getName())) + .forEach((field) -> checkPropertyImplementation(field, allowedImplementations, events))); + } + + private static void checkPropertyImplementation(JavaField field, List allowedImplementations, + ConditionEvents events) { + field.getAccessesToSelf() + .stream() + .filter((access) -> access.getAccessType() == AccessType.SET) + .flatMap((access) -> initializerConstructorCalls(access).stream()) + .filter((call) -> !allowedImplementations.contains(call.getTargetOwner().getName())) + .forEach((call) -> addViolation(events, field, + "%s should be initialized with %s instead of %s".formatted(field.getDescription(), + String.join(" or ", allowedImplementations), call.getTargetOwner().getName()))); + } + + private static List initializerConstructorCalls(JavaFieldAccess fieldAccess) { + return fieldAccess.getOrigin() + .getConstructorCallsFromSelf() + .stream() + .filter((call) -> call.getLineNumber() == fieldAccess.getLineNumber()) + .toList(); + } + + private static DescribedPredicate areNestedInClassAnnotatedWith(String annotationClass) { + return DescribedPredicate.describe("one of its inner class", + (javaClass) -> javaClass.getEnclosingClass() + .map((enclosing) -> enclosing.isAnnotatedWith(annotationClass) + || areNestedInClassAnnotatedWith(annotationClass).test(enclosing)) + .orElse(false)); + } + private static ArchRule autoConfigurationClassesShouldBePublicAndFinal() { return ArchRuleDefinition.classes() .that(areRegularAutoConfiguration()) diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java index 6bc68a37a3f7..8a40b343ee71 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/BomExtension.java @@ -95,6 +95,13 @@ public List getLibraries() { return this.libraries; } + public Library getLibrary(String name) { + return getLibraries().stream() + .filter((library) -> library.getName().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No library found named '%s'".formatted(name))); + } + public void upgrade(Action action) { action.execute(this.upgradeHandler); } diff --git a/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java b/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java index 6d8d2ab75349..9d2a9cec3436 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/bom/Library.java @@ -668,6 +668,9 @@ private Library findManagingLibrary() { } private List getBomDependencies(Library manager) { + if (manager == null) { + return Collections.emptyList(); + } return manager.getGroups() .stream() .flatMap((group) -> group.getBoms() diff --git a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java index 457ccda4da86..eee7ebd81af3 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/context/properties/DocumentConfigurationProperties.java @@ -63,6 +63,7 @@ void documentConfigurationProperties() throws IOException { Snippets snippets = new Snippets(this.configurationPropertyMetadata, getDeprecated().getOrElse(false)); snippets.add("application-properties.core", "Core Properties", this::corePrefixes); snippets.add("application-properties.cache", "Cache Properties", this::cachePrefixes); + snippets.add("application-properties.grpc", "gRPC Properties", this::grpcPrefixes); snippets.add("application-properties.mail", "Mail Properties", this::mailPrefixes); snippets.add("application-properties.json", "JSON Properties", this::jsonPrefixes); snippets.add("application-properties.data", "Data Properties", this::dataPrefixes); @@ -115,6 +116,10 @@ private void cachePrefixes(Config config) { config.accept("spring.cache"); } + private void grpcPrefixes(Config prefix) { + prefix.accept("spring.grpc"); + } + private void mailPrefixes(Config config) { config.accept("spring.mail"); config.accept("spring.sendgrid"); @@ -165,6 +170,7 @@ private void dataMigrationPrefixes(Config prefix) { private void integrationPrefixes(Config prefix) { prefix.accept("spring.activemq"); + prefix.accept("spring.amqp"); prefix.accept("spring.artemis"); prefix.accept("spring.batch"); prefix.accept("spring.integration"); diff --git a/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java b/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java index 6ce52decf6e2..a170a038c71a 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainExtension.java @@ -21,33 +21,31 @@ import org.gradle.api.provider.Property; import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.springframework.boot.build.SystemRequirementsExtension; + /** * DSL extension for {@link ToolchainPlugin}. * * @author Christoph Dreis */ -public class ToolchainExtension { - - private final Property maximumCompatibleJavaVersion; - - private final ListProperty testJvmArgs; +public abstract class ToolchainExtension { private final JavaLanguageVersion javaVersion; public ToolchainExtension(Project project) { - this.maximumCompatibleJavaVersion = project.getObjects().property(JavaLanguageVersion.class); - this.testJvmArgs = project.getObjects().listProperty(String.class); String toolchainVersion = (String) project.findProperty("toolchainVersion"); this.javaVersion = (toolchainVersion != null) ? JavaLanguageVersion.of(toolchainVersion) : null; + SystemRequirementsExtension systemRequirements = project.getExtensions() + .getByType(SystemRequirementsExtension.class); + getMinimumCompatibleJavaVersion() + .convention(project.provider(() -> JavaLanguageVersion.of(systemRequirements.getJava().getVersion()))); } - public Property getMaximumCompatibleJavaVersion() { - return this.maximumCompatibleJavaVersion; - } + public abstract Property getMinimumCompatibleJavaVersion(); - public ListProperty getTestJvmArgs() { - return this.testJvmArgs; - } + public abstract Property getMaximumCompatibleJavaVersion(); + + public abstract ListProperty getTestJvmArgs(); JavaLanguageVersion getJavaVersion() { return this.javaVersion; diff --git a/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainPlugin.java b/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainPlugin.java index 6b37f10b6ef4..14c95b57d5b2 100644 --- a/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainPlugin.java +++ b/buildSrc/src/main/java/org/springframework/boot/build/toolchain/ToolchainPlugin.java @@ -53,9 +53,13 @@ private void configure(Project project, ToolchainExtension toolchain) { } private boolean isJavaVersionSupported(ToolchainExtension toolchain, JavaLanguageVersion toolchainVersion) { - return toolchain.getMaximumCompatibleJavaVersion() - .map((version) -> version.canCompileOrRun(toolchainVersion)) - .getOrElse(true); + JavaLanguageVersion minimumVersion = toolchain.getMinimumCompatibleJavaVersion().getOrNull(); + if (minimumVersion == null || toolchainVersion.canCompileOrRun(minimumVersion)) { + return toolchain.getMaximumCompatibleJavaVersion() + .map((version) -> version.canCompileOrRun(toolchainVersion)) + .getOrElse(true); + } + return false; } private void disableToolchainTasks(Project project) { diff --git a/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties b/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties index 23061f7cf278..1797a4028fd2 100644 --- a/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties +++ b/buildSrc/src/main/resources/org/springframework/boot/build/antora/antora-asciidoc-attributes.properties @@ -26,6 +26,7 @@ url-gradle-docs-java-plugin={url-gradle-docs}/java_plugin.html url-gradle-docs-war-plugin={url-gradle-docs}/war_plugin.html url-gradle-dsl=https://docs.gradle.org/current/dsl url-gradle-javadoc=https://docs.gradle.org/current/javadoc +url-grpc-api-javadoc=https://javadoc.io/doc/io.grpc/grpc-api/{version-grpc-api} url-kotlin-docs-kotlin-plugin={url-kotlin-docs}/using-gradle.html url-micrometer-docs-concepts={url-micrometer-docs}/concepts url-micrometer-docs-implementations={url-micrometer-docs}/implementations @@ -35,6 +36,7 @@ url-native-build-tools-docs-gradle-plugin={url-native-build-tools-docs}/gradle-p url-native-build-tools-docs-maven-plugin={url-native-build-tools-docs}/maven-plugin.html url-paketo-docs=https://paketo.io/docs url-paketo-docs-java-buildpack={url-paketo-docs}/buildpacks/language-family-buildpacks/java +url-protobuf-docs-gradle-plugin=https://github.com/google/protobuf-gradle-plugin url-pulsar-client-api-javadoc=https://javadoc.io/doc/org.apache.pulsar/pulsar-client-api/{version-pulsar-client-api} url-spring-boot-for-apache-geode-docs=https://docs.spring.io/spring-boot-data-geode-build/2.0.x/reference/html5 url-spring-boot-for-apache-geode-site=https://github.com/spring-projects/spring-boot-data-geode @@ -81,6 +83,8 @@ url-jackson-databind-javadoc=https://javadoc.io/doc/tools.jackson.core/jackson-d url-jackson-dataformat-xml-javadoc=https://javadoc.io/doc/tools.jackson.dataformat/jackson-dataformat-xml/{version-jackson-dataformat-xml} url-jackson2-databind-javadoc=https://javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/{version-jackson2-databind} +https://javadoc.io/doc/io.grpc/grpc-api/latest/io/grpc/package-summary.html + # === Javadoc Locations === javadoc-location-com-fasterxml-jackson-annotation={url-jackson-annotations-javadoc} @@ -103,6 +107,7 @@ javadoc-location-org-springframework-data-rest={url-spring-data-rest-javadoc} javadoc-location-tools-jackson-core={url-jackson-core-javadoc} javadoc-location-tools-jackson-databind={url-jackson-databind-javadoc} javadoc-location-tools-jackson-dataformat-xml={url-jackson-dataformat-xml-javadoc} +javadoc-location-io-grpc={url-grpc-api-javadoc} # === API References === @@ -117,6 +122,7 @@ apiref-openjdk=https://docs.oracle.com/en/java/javase/17/docs/api code-spring-boot=https://github.com/{github-repo}/tree/{github-ref} code-spring-boot-autoconfigure-src={code-spring-boot}/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure +code-spring-boot-batch-data-mongodb-src={code-spring-boot}/module/spring-boot-batch-data-mongodb/src/main/java/org/springframework/boot/batch/mongodb code-spring-boot-batch-jdbc-src={code-spring-boot}/module/spring-boot-batch-jdbc/src/main/java/org/springframework/boot/batch/jdbc code-spring-boot-batch-src={code-spring-boot}/module/spring-boot-batch/src/main/java/org/springframework/boot/batch code-spring-boot-freemarker-src={code-spring-boot}/module/spring-boot-freemarker/src/main/java/org/springframework/boot/freemarker diff --git a/buildSrc/src/test/java/org/springframework/boot/build/antora/AntoraAsciidocAttributesTests.java b/buildSrc/src/test/java/org/springframework/boot/build/antora/AntoraAsciidocAttributesTests.java index 25a843c203c6..1ae6f884963e 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/antora/AntoraAsciidocAttributesTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/antora/AntoraAsciidocAttributesTests.java @@ -287,6 +287,7 @@ private Map mockDependencyVersions(String version) { addMockJacksonCoreVersion(versions, "jackson-core", version); addMockJacksonCoreVersion(versions, "jackson-databind", version); addMockJacksonCoreVersion(versions, "jackson-databind", version); + versions.put("io.grpc:grpc-api", version); versions.put("org.apache.pulsar:pulsar-client-api", version); versions.put("tools.jackson.dataformat:jackson-dataformat-xml", version); return versions; diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java index a96dbdf081df..62224bcefa95 100644 --- a/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/ArchitectureCheckTests.java @@ -47,6 +47,7 @@ import org.springframework.boot.build.architecture.annotations.TestConditionalOnMissingBean; import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesBinding; +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; import org.springframework.boot.build.architecture.annotations.TestDeprecatedConfigurationProperty; import org.springframework.util.ClassUtils; import org.springframework.util.FileSystemUtils; @@ -61,13 +62,19 @@ * @author Scott Frederick * @author Ivan Malutin * @author Dmytro Nosan + * @author Stefano Cordio + * @author Venkata Naga Sai Srikanth Gollapudi */ class ArchitectureCheckTests { - private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.9"; + private static final String ASSERTJ_CORE = "org.assertj:assertj-core:3.27.4"; private static final String JUNIT_JUPITER = "org.junit.jupiter:junit-jupiter:5.12.0"; + private static final String SPRING_CONTEXT = "org.springframework:spring-context:6.2.15"; + + private static final String SPRING_CORE = "org.springframework:spring-core:6.2.15"; + private static final String SPRING_INTEGRATION_JMX = "org.springframework.integration:spring-integration-jmx:6.5.1"; private GradleBuild gradleBuild; @@ -326,6 +333,76 @@ void whenMethodLevelConfigurationPropertiesContainsOnlyValueShouldSucceedAndWrit Task.CHECK_ARCHITECTURE_MAIN); } + @Test + void whenConfigurationPropertiesUsesHashMapShouldFailAndWriteReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/hashmap", "annotations"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashMap"); + } + + @Test + void whenConfigurationPropertiesSourceUsesHashMapShouldFailAndWriteReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/hashmap", "annotations"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashMap"); + } + + @Test + void whenConfigurationPropertiesUsesHashSetShouldFailAndWriteReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/hashset", "annotations"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashSet"); + } + + @Test + void whenConfigurationPropertiesSourceUsesHashSetShouldFailAndWriteReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/hashset", "annotations"); + buildAndFail(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN, "should be initialized with java.util.LinkedHashSet"); + } + + @Test + void whenConfigurationPropertiesUsesLinkedHashMapShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashmap", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + + @Test + void whenConfigurationPropertiesSourceUsesLinkedHashMapShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/linkedhashmap", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + + @Test + void whenConfigurationPropertiesUsesEnumMapShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/enummap", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + + @Test + void whenConfigurationPropertiesSourceUsesEnumMapShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/enummap", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + + @Test + void whenConfigurationPropertiesUsesLinkedHashSetShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/linkedhashset", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + + @Test + void whenConfigurationPropertiesSourceUsesLinkedHashSetShouldSucceedAndWriteEmptyReport() throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationpropertiessource/linkedhashset", "annotations"); + build(this.gradleBuild.withDependencies(SPRING_CONTEXT).withConfigurationPropertiesSourceAnnotation(), + Task.CHECK_ARCHITECTURE_MAIN); + } + @Test void whenConfigurationPropertiesBindingBeanMethodIsNotStaticShouldFailAndWriteReport() throws IOException { prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "configurationproperties/bindingnonstatic", "annotations"); @@ -429,6 +506,23 @@ void whenDeprecatedConfigurationPropertyIsMissingSinceShouldFailAndWriteReport() "DeprecatedConfigurationPropertySince.getProperty"); } + @Test + void whenCustomAssertionMethodNotReturningSelfIsAnnotatedWithCheckReturnValueShouldSucceedAndWriteEmptyReport() + throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "assertj/checkReturnValue"); + build(this.gradleBuild.withDependencies(ASSERTJ_CORE, SPRING_CORE), Task.CHECK_ARCHITECTURE_MAIN); + } + + @Test + void whenCustomAssertionMethodNotReturningSelfIsNotAnnotatedWithCheckReturnValueShouldFailAndWriteReport() + throws IOException { + prepareTask(Task.CHECK_ARCHITECTURE_MAIN, "assertj/noCheckReturnValue"); + buildAndFail(this.gradleBuild.withDependencies(ASSERTJ_CORE), Task.CHECK_ARCHITECTURE_MAIN, + "methods that are declared in classes that implement org.assertj.core.api.Assert and " + + "are public and do not have modifier BRIDGE and do not return self type should be annotated " + + "with @CheckReturnValue"); + } + private void prepareTask(Task task, String... sourceDirectories) throws IOException { for (String sourceDirectory : sourceDirectories) { FileSystemUtils.copyRecursively( @@ -546,6 +640,12 @@ GradleBuild withConfigurationPropertiesAnnotation() { return this; } + GradleBuild withConfigurationPropertiesSourceAnnotation() { + configureTasks(ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_SOURCE.name(), + TestConfigurationPropertiesSource.class.getName()); + return this; + } + GradleBuild withConfigurationPropertiesBindingAnnotation() { configureTasks(ArchitectureCheckAnnotation.CONFIGURATION_PROPERTIES_BINDING.name(), TestConfigurationPropertiesBinding.class.getName()); diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/annotations/TestConfigurationPropertiesSource.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/annotations/TestConfigurationPropertiesSource.java new file mode 100644 index 000000000000..d4529eceef43 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/annotations/TestConfigurationPropertiesSource.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-present 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.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface TestConfigurationPropertiesSource { + +} diff --git a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/NoopSender.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/checkReturnValue/WithCheckReturnValue.java similarity index 56% rename from module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/NoopSender.java rename to buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/checkReturnValue/WithCheckReturnValue.java index 655f6282eba5..74845f9dbd86 100644 --- a/module/spring-boot-micrometer-tracing-opentelemetry/src/test/java/org/springframework/boot/micrometer/tracing/opentelemetry/autoconfigure/zipkin/NoopSender.java +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/checkReturnValue/WithCheckReturnValue.java @@ -14,31 +14,26 @@ * limitations under the License. */ -package org.springframework.boot.micrometer.tracing.opentelemetry.autoconfigure.zipkin; +package org.springframework.boot.build.architecture.assertj.checkReturnValue; -import java.io.IOException; -import java.util.List; +import org.assertj.core.api.AbstractAssert; -import zipkin2.reporter.BytesMessageSender; -import zipkin2.reporter.Encoding; +import org.springframework.lang.CheckReturnValue; -class NoopSender extends BytesMessageSender.Base { +public class WithCheckReturnValue extends AbstractAssert { - NoopSender(Encoding encoding) { - super(encoding); + WithCheckReturnValue() { + super(null, WithCheckReturnValue.class); } - @Override - public int messageMaxBytes() { - return 1024; - } - - @Override - public void send(List encodedSpans) { + @CheckReturnValue + public Object notReturningSelf() { + return new Object(); } @Override - public void close() throws IOException { + public WithCheckReturnValue isEqualTo(Object expected) { + return super.isEqualTo(expected); } } diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/noCheckReturnValue/NoCheckReturnValue.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/noCheckReturnValue/NoCheckReturnValue.java new file mode 100644 index 000000000000..70e4be97c7d3 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/assertj/noCheckReturnValue/NoCheckReturnValue.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-present 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.assertj.noCheckReturnValue; + +import org.assertj.core.api.AbstractAssert; + +public class NoCheckReturnValue extends AbstractAssert { + + NoCheckReturnValue() { + super(null, NoCheckReturnValue.class); + } + + public Object notReturningSelf() { + return new Object(); + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/enummap/ConfigurationPropertiesWithEnumMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/enummap/ConfigurationPropertiesWithEnumMap.java new file mode 100644 index 000000000000..f3fa73b9cb30 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/enummap/ConfigurationPropertiesWithEnumMap.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-present 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.configurationproperties.enummap; + +import java.util.EnumMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; + +/** + * Test {@link TestConfigurationProperties} using {@link EnumMap}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + */ +@TestConfigurationProperties("testing") +public class ConfigurationPropertiesWithEnumMap { + + private Map properties = new EnumMap<>(Example.class); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + enum Example { + + ONE + + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashmap/ConfigurationPropertiesWithHashMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashmap/ConfigurationPropertiesWithHashMap.java new file mode 100644 index 000000000000..1668ccc84af7 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashmap/ConfigurationPropertiesWithHashMap.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present 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.configurationproperties.hashmap; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; + +/** + * Test {@link TestConfigurationProperties} using {@link HashMap}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + */ +@TestConfigurationProperties("testing") +public class ConfigurationPropertiesWithHashMap { + + private Map properties = new HashMap<>(); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashset/ConfigurationPropertiesWithHashSet.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashset/ConfigurationPropertiesWithHashSet.java new file mode 100644 index 000000000000..f8bfc8497d91 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/hashset/ConfigurationPropertiesWithHashSet.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present 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.configurationproperties.hashset; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; + +/** + * Test {@link TestConfigurationProperties} using {@link HashSet}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + */ +@TestConfigurationProperties("testing") +public class ConfigurationPropertiesWithHashSet { + + private Set items = new HashSet<>(); + + public Set getItems() { + return this.items; + } + + public void setItems(Set items) { + this.items = items; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java new file mode 100644 index 000000000000..b8a8ed38a98f --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present 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.configurationproperties.linkedhashmap; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; + +/** + * Test {@link TestConfigurationProperties} using {@link LinkedHashMap}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + */ +@TestConfigurationProperties("testing") +public class ConfigurationPropertiesWithLinkedHashMap { + + private Map properties = new LinkedHashMap<>(); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java new file mode 100644 index 000000000000..d75c7db3f797 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationproperties/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present 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.configurationproperties.linkedhashset; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationProperties; + +/** + * Test {@link TestConfigurationProperties} using {@link LinkedHashSet}. + * + * @author Venkata Naga Sai Srikanth Gollapudi + */ +@TestConfigurationProperties("testing") +public class ConfigurationPropertiesWithLinkedHashSet { + + private Set items = new LinkedHashSet<>(); + + public Set getItems() { + return this.items; + } + + public void setItems(Set items) { + this.items = items; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/enummap/ConfigurationPropertiesWithEnumMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/enummap/ConfigurationPropertiesWithEnumMap.java new file mode 100644 index 000000000000..1c5534177a25 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/enummap/ConfigurationPropertiesWithEnumMap.java @@ -0,0 +1,48 @@ +/* + * Copyright 2012-present 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.configurationpropertiessource.enummap; + +import java.util.EnumMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; + +/** + * Test {@link TestConfigurationPropertiesSource} using {@link EnumMap}. + * + * @author Stephane Nicoll + */ +@TestConfigurationPropertiesSource +public class ConfigurationPropertiesWithEnumMap { + + private Map properties = new EnumMap<>(Example.class); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + enum Example { + + ONE + + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashmap/ConfigurationPropertiesWithHashMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashmap/ConfigurationPropertiesWithHashMap.java new file mode 100644 index 000000000000..98d6b4e26b4b --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashmap/ConfigurationPropertiesWithHashMap.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present 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.configurationpropertiessource.hashmap; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; + +/** + * Test {@link TestConfigurationPropertiesSource} using {@link HashMap}. + * + * @author Stephane Nicoll + */ +@TestConfigurationPropertiesSource +public class ConfigurationPropertiesWithHashMap { + + private Map properties = new HashMap<>(); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashset/ConfigurationPropertiesWithHashSet.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashset/ConfigurationPropertiesWithHashSet.java new file mode 100644 index 000000000000..c7b4011f9d99 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/hashset/ConfigurationPropertiesWithHashSet.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present 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.configurationpropertiessource.hashset; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; + +/** + * Test {@link TestConfigurationPropertiesSource} using {@link HashSet}. + * + * @author Stephane Nicoll + */ +@TestConfigurationPropertiesSource +public class ConfigurationPropertiesWithHashSet { + + private Set items = new HashSet<>(); + + public Set getItems() { + return this.items; + } + + public void setItems(Set items) { + this.items = items; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java new file mode 100644 index 000000000000..d300433a0429 --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashmap/ConfigurationPropertiesWithLinkedHashMap.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present 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.configurationpropertiessource.linkedhashmap; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; + +/** + * Test {@link TestConfigurationPropertiesSource} using {@link LinkedHashMap}. + * + * @author Stephane Nicoll + */ +@TestConfigurationPropertiesSource +public class ConfigurationPropertiesWithLinkedHashMap { + + private Map properties = new LinkedHashMap<>(); + + public Map getProperties() { + return this.properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + +} diff --git a/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java new file mode 100644 index 000000000000..085fea8b58df --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/boot/build/architecture/configurationpropertiessource/linkedhashset/ConfigurationPropertiesWithLinkedHashSet.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-present 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.configurationpropertiessource.linkedhashset; + +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.boot.build.architecture.annotations.TestConfigurationPropertiesSource; + +/** + * Test {@link TestConfigurationPropertiesSource} using {@link LinkedHashSet}. + * + * @author Stephane Nicoll + */ +@TestConfigurationPropertiesSource +public class ConfigurationPropertiesWithLinkedHashSet { + + private Set items = new LinkedHashSet<>(); + + public Set getItems() { + return this.items; + } + + public void setItems(Set items) { + this.items = items; + } + +} diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java index bdbf888926c2..f3f996311da0 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/BuildRequest.java @@ -95,11 +95,11 @@ public class BuildRequest { private final List tags; - private final @Nullable Cache buildWorkspace; + private final @Nullable LocalCache buildWorkspace; private final @Nullable Cache buildCache; - private final @Nullable Cache launchCache; + private final @Nullable LocalCache launchCache; private final @Nullable Instant createdDate; @@ -140,8 +140,8 @@ public class BuildRequest { @Nullable Boolean trustBuilder, @Nullable ImageReference runImage, Creator creator, Map env, boolean cleanCache, boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List buildpacks, List bindings, @Nullable String network, - List tags, @Nullable Cache buildWorkspace, @Nullable Cache buildCache, - @Nullable Cache launchCache, @Nullable Instant createdDate, @Nullable String applicationDirectory, + List tags, @Nullable LocalCache buildWorkspace, @Nullable Cache buildCache, + @Nullable LocalCache launchCache, @Nullable Instant createdDate, @Nullable String applicationDirectory, @Nullable List securityOptions, @Nullable ImagePlatform platform) { this.name = name; this.applicationContent = applicationContent; @@ -395,7 +395,7 @@ public BuildRequest withTags(List tags) { * @return an updated build request * @since 3.2.0 */ - public BuildRequest withBuildWorkspace(Cache buildWorkspace) { + public BuildRequest withBuildWorkspace(LocalCache buildWorkspace) { Assert.notNull(buildWorkspace, "'buildWorkspace' must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, @@ -421,7 +421,7 @@ public BuildRequest withBuildCache(Cache buildCache) { * @param launchCache the cache * @return an updated build request */ - public BuildRequest withLaunchCache(Cache launchCache) { + public BuildRequest withLaunchCache(LocalCache launchCache) { Assert.notNull(launchCache, "'launchCache' must not be null"); return new BuildRequest(this.name, this.applicationContent, this.builder, this.trustBuilder, this.runImage, this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, @@ -632,7 +632,7 @@ public List getTags() { * @return the build workspace or {@code null} * @since 3.2.0 */ - public @Nullable Cache getBuildWorkspace() { + public @Nullable LocalCache getBuildWorkspace() { return this.buildWorkspace; } @@ -648,7 +648,7 @@ public List getTags() { * Return the custom launch cache that should be used by the lifecycle. * @return the launch cache */ - public @Nullable Cache getLaunchCache() { + public @Nullable LocalCache getLaunchCache() { return this.launchCache; } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java index a3e4f98a815b..ce4c20032f5d 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Cache.java @@ -16,69 +16,46 @@ package org.springframework.boot.buildpack.platform.build; -import java.util.Objects; - import org.jspecify.annotations.Nullable; +import org.springframework.boot.buildpack.platform.build.LocalCache.Bind; +import org.springframework.boot.buildpack.platform.build.LocalCache.Volume; import org.springframework.boot.buildpack.platform.docker.type.VolumeName; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * Details of a cache for use by the CNB builder. * * @author Scott Frederick + * @author Tim Ysewyn + * @author Stephane Nicoll * @since 2.6.0 */ -public class Cache { - - /** - * The format of the cache. - */ - public enum Format { - - /** - * A cache stored as a volume in the Docker daemon. - */ - VOLUME("volume"), - - /** - * A cache stored as a bind mount. - */ - BIND("bind mount"); - - private final String description; - - Format(String description) { - this.description = description; - } - - public String getDescription() { - return this.description; - } - - } - - protected final Format format; - - Cache(Format format) { - this.format = format; - } +public sealed interface Cache permits LocalCache, ImageCache { /** * Return the details of the cache if it is a volume cache. * @return the cache, or {@code null} if it is not a volume cache */ - public @Nullable Volume getVolume() { - return (this.format.equals(Format.VOLUME)) ? (Volume) this : null; + default LocalCache.@Nullable Volume getVolume() { + return (this instanceof LocalCache.Volume volume) ? volume : null; } /** * Return the details of the cache if it is a bind cache. * @return the cache, or {@code null} if it is not a bind cache */ - public @Nullable Bind getBind() { - return (this.format.equals(Format.BIND)) ? (Bind) this : null; + default LocalCache.@Nullable Bind getBind() { + return (this instanceof LocalCache.Bind bind) ? bind : null; + } + + /** + * Return the details of the cache if it is an image cache. + * @return the cache, or {@code null} if it is not an image cache + * @since 4.2.0 + */ + default @Nullable ImageCache getImage() { + return (this instanceof ImageCache image) ? image : null; } /** @@ -86,7 +63,7 @@ public String getDescription() { * @param name the cache volume name * @return a new cache instance */ - public static Cache volume(String name) { + static LocalCache volume(String name) { Assert.notNull(name, "'name' must not be null"); return new Volume(VolumeName.of(name)); } @@ -96,7 +73,7 @@ public static Cache volume(String name) { * @param name the cache volume name * @return a new cache instance */ - public static Cache volume(VolumeName name) { + static LocalCache volume(VolumeName name) { Assert.notNull(name, "'name' must not be null"); return new Volume(name); } @@ -106,120 +83,20 @@ public static Cache volume(VolumeName name) { * @param source the cache bind mount source * @return a new cache instance */ - public static Cache bind(String source) { + static LocalCache bind(String source) { Assert.notNull(source, "'source' must not be null"); return new Bind(source); } - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - Cache other = (Cache) obj; - return Objects.equals(this.format, other.format); - } - - @Override - public int hashCode() { - return ObjectUtils.nullSafeHashCode(this.format); - } - /** - * Details of a cache stored in a Docker volume. - */ - public static class Volume extends Cache { - - private final VolumeName name; - - Volume(VolumeName name) { - super(Format.VOLUME); - this.name = name; - } - - public String getName() { - return this.name.toString(); - } - - public VolumeName getVolumeName() { - return this.name; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (!super.equals(obj)) { - return false; - } - Volume other = (Volume) obj; - return Objects.equals(this.name, other.name); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + ObjectUtils.nullSafeHashCode(this.name); - return result; - } - - @Override - public String toString() { - return this.format.getDescription() + " '" + this.name + "'"; - } - - } - - /** - * Details of a cache stored in a bind mount. + * Create a new {@code Cache} that uses an image with the provided name. + * @param name the cache image name + * @return a new cache instance + * @since 4.2.0 */ - public static class Bind extends Cache { - - private final String source; - - Bind(String source) { - super(Format.BIND); - this.source = source; - } - - public String getSource() { - return this.source; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - if (!super.equals(obj)) { - return false; - } - Bind other = (Bind) obj; - return Objects.equals(this.source, other.source); - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + ObjectUtils.nullSafeHashCode(this.source); - return result; - } - - @Override - public String toString() { - return this.format.getDescription() + " '" + this.source + "'"; - } - + static ImageCache image(String name) { + Assert.notNull(name, "'name' must not be null"); + return new ImageCache(name); } } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageCache.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageCache.java new file mode 100644 index 000000000000..98f8a3a4eede --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/ImageCache.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-present 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.buildpack.platform.build; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.ObjectUtils; + +/** + * Details of a cache stored in an image. + * + * @author Tim Ysewyn + * @author Stephane Nicoll + * @since 4.2.0 + */ +public final class ImageCache implements Cache { + + private final String name; + + ImageCache(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ImageCache other = (ImageCache) obj; + return Objects.equals(this.name, other.name); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.name); + } + + @Override + public String toString() { + return "image '" + this.name + "'"; + } + +} diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java index b5467db65173..a4b1dc841f07 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Lifecycle.java @@ -27,7 +27,6 @@ import com.sun.jna.Platform; import org.jspecify.annotations.Nullable; -import org.springframework.boot.buildpack.platform.build.Cache.Bind; import org.springframework.boot.buildpack.platform.docker.ApiVersion; import org.springframework.boot.buildpack.platform.docker.DockerApi; import org.springframework.boot.buildpack.platform.docker.LogUpdateEvent; @@ -78,13 +77,13 @@ class Lifecycle implements Closeable { private final ApiVersion platformVersion; - private final Cache layers; + private final LocalCache layers; - private final Cache application; + private final LocalCache application; private final Cache buildCache; - private final Cache launchCache; + private final LocalCache launchCache; private final String applicationDirectory; @@ -130,7 +129,7 @@ private Cache getBuildCache(BuildRequest request) { return createVolumeCache(request, "build"); } - private Cache getLaunchCache(BuildRequest request) { + private LocalCache getLaunchCache(BuildRequest request) { if (request.getLaunchCache() != null) { return request.getLaunchCache(); } @@ -195,7 +194,7 @@ private Phase createPhase() { Assert.state(runImage != null, "'runImage' must not be null"); phase.withRunImage(runImage); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); - phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + configureBuildCache(phase); phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); configureDaemonAccess(phase); @@ -215,6 +214,9 @@ private Phase createPhase() { private Phase analyzePhase() { Phase phase = new Phase("analyzer", isVerboseLogging()); configureDaemonAccess(phase); + if (this.buildCache instanceof ImageCache imageBuildCache) { + phase.withBuildCache(imageBuildCache.getName()); + } phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); @@ -239,7 +241,7 @@ private Phase detectPhase() { private Phase restorePhase() { Phase phase = new Phase("restorer", isVerboseLogging()); configureDaemonAccess(phase); - phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + configureBuildCache(phase); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); configureOptions(phase); return phase; @@ -260,7 +262,7 @@ private Phase exportPhase() { configureDaemonAccess(phase); phase.withApp(this.applicationDirectory, Binding.from(getCacheBindingSource(this.application), this.applicationDirectory)); - phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(this.buildCache), Directory.CACHE)); + configureBuildCache(phase); phase.withLaunchCache(Directory.LAUNCH_CACHE, Binding.from(getCacheBindingSource(this.launchCache), Directory.LAUNCH_CACHE)); phase.withLayers(Directory.LAYERS, Binding.from(getCacheBindingSource(this.layers), Directory.LAYERS)); @@ -273,44 +275,43 @@ private Phase exportPhase() { return phase; } - private Cache getLayersBindingSource(BuildRequest request) { + private LocalCache getLayersBindingSource(BuildRequest request) { if (request.getBuildWorkspace() != null) { return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "layers"); } return createVolumeCache("pack-layers-"); } - private Cache getApplicationBindingSource(BuildRequest request) { + private LocalCache getApplicationBindingSource(BuildRequest request) { if (request.getBuildWorkspace() != null) { return getBuildWorkspaceBindingSource(request.getBuildWorkspace(), "app"); } return createVolumeCache("pack-app-"); } - private Cache getBuildWorkspaceBindingSource(Cache buildWorkspace, String suffix) { + private LocalCache getBuildWorkspaceBindingSource(LocalCache buildWorkspace, String suffix) { if (buildWorkspace.getVolume() != null) { return Cache.volume(buildWorkspace.getVolume().getName() + "-" + suffix); } - - Bind bind = buildWorkspace.getBind(); + LocalCache.Bind bind = buildWorkspace.getBind(); Assert.state(bind != null, "'bind' must not be null"); return Cache.bind(bind.getSource() + "-" + suffix); } - private String getCacheBindingSource(Cache cache) { + private String getCacheBindingSource(LocalCache cache) { if (cache.getVolume() != null) { return cache.getVolume().getName(); } - Bind bind = cache.getBind(); + LocalCache.Bind bind = cache.getBind(); Assert.state(bind != null, "'bind' must not be null"); return bind.getSource(); } - private Cache createVolumeCache(String prefix) { + private LocalCache createVolumeCache(String prefix) { return Cache.volume(createRandomVolumeName(prefix)); } - private Cache createVolumeCache(BuildRequest request, String suffix) { + private LocalCache createVolumeCache(BuildRequest request, String suffix) { return Cache.volume( VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", "." + suffix, 6)); } @@ -343,6 +344,15 @@ private void configureDaemonAccess(Phase phase) { } } + private void configureBuildCache(Phase phase) { + if (this.buildCache instanceof ImageCache image) { + phase.withBuildCache(image.getName()); + } + else if (this.buildCache instanceof LocalCache localCache) { + phase.withBuildCache(Directory.CACHE, Binding.from(getCacheBindingSource(localCache), Directory.CACHE)); + } + } + private void configureCreatedDate(Phase phase) { if (this.request.getCreatedDate() != null) { phase.withEnv(SOURCE_DATE_EPOCH_KEY, Long.toString(this.request.getCreatedDate().getEpochSecond())); @@ -421,7 +431,7 @@ private void deleteVolume(VolumeName name) throws IOException { this.docker.volume().delete(name, true); } - private void deleteBind(Cache.Bind bind) { + private void deleteBind(LocalCache.Bind bind) { try { FileSystemUtils.deleteRecursively(Path.of(bind.getSource())); } diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LocalCache.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LocalCache.java new file mode 100644 index 000000000000..4a46d4c9c95a --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/LocalCache.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-present 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.buildpack.platform.build; + +import java.util.Objects; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.buildpack.platform.docker.type.VolumeName; +import org.springframework.util.ObjectUtils; + +/** + * Details of a local cache stored as a volume or bind mount. + * + * @author Scott Frederick + * @author Stephane Nicoll + * @since 4.2.0 + */ +public sealed interface LocalCache extends Cache permits LocalCache.Volume, LocalCache.Bind { + + /** + * Details of a cache stored in a Docker volume. + */ + final class Volume implements LocalCache { + + private final VolumeName name; + + Volume(VolumeName name) { + this.name = name; + } + + public String getName() { + return this.name.toString(); + } + + public VolumeName getVolumeName() { + return this.name; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Volume other = (Volume) obj; + return Objects.equals(this.name, other.name); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.name); + } + + @Override + public String toString() { + return "volume '" + this.name + "'"; + } + + } + + /** + * Details of a cache stored in a bind mount. + */ + final class Bind implements LocalCache { + + private final String source; + + Bind(String source) { + this.source = source; + } + + public String getSource() { + return this.source; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + Bind other = (Bind) obj; + return Objects.equals(this.source, other.source); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(this.source); + } + + @Override + public String toString() { + return "bind mount '" + this.source + "'"; + } + + } + +} diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java index d037d33b072e..985e6992b875 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/build/Phase.java @@ -75,6 +75,10 @@ void withBuildCache(String path, Binding binding) { withBinding(binding); } + void withBuildCache(String image) { + withArgs("-cache-image", image); + } + /** * Update this phase with Docker daemon access. */ diff --git a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java index 6169667708d1..e2786a90bb02 100644 --- a/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java +++ b/buildpack/spring-boot-buildpack-platform/src/main/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelper.java @@ -41,6 +41,10 @@ class CredentialHelper { private static final String USR_LOCAL_BIN = "/usr/local/bin/"; + private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin/"; + + private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN }; + private static final Set CREDENTIAL_NOT_FOUND_MESSAGES = Set.of("credentials not found in native keychain", "no credentials server URL", "no credentials username"); @@ -92,16 +96,19 @@ private Process start(ProcessBuilder processBuilder) throws IOException { if (!Platform.isMac()) { throw ex; } - try { - List command = new ArrayList<>(processBuilder.command()); - command.set(0, USR_LOCAL_BIN + command.get(0)); - return processBuilder.command(command).start(); - } - catch (Exception suppressed) { - // Suppresses the exception and rethrows the original exception - ex.addSuppressed(suppressed); - throw ex; + List originalCommand = List.copyOf(processBuilder.command()); + String executable = originalCommand.get(0); + for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { + List command = new ArrayList<>(originalCommand); + command.set(0, binDirectory + executable); + try { + return processBuilder.command(command).start(); + } + catch (Exception suppressed) { + ex.addSuppressed(suppressed); + } } + throw ex; } } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java index 7bbb13d3e72a..19ca097fc3dd 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/BuildRequestTests.java @@ -325,6 +325,14 @@ void withBuildBindCacheAddsCache() throws IOException { assertThat(withCache.getBuildCache()).isEqualTo(Cache.bind("/tmp/build-cache")); } + @Test + void withBuildImageCacheAddsCache() throws IOException { + BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar")); + BuildRequest withCache = request.withBuildCache(Cache.image("build-cache-image")); + assertThat(request.getBuildCache()).isNull(); + assertThat(withCache.getBuildCache()).isEqualTo(Cache.image("build-cache-image")); + } + @Test @SuppressWarnings("NullAway") // Test null check void withBuildVolumeCacheWhenCacheIsNullThrowsException() throws IOException { diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java index 69c8b8f477b3..eac76947b9fb 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/LifecycleTests.java @@ -291,6 +291,27 @@ void executeWithCacheBindMountsExecutesPhases(boolean trustBuilder) throws Excep assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); } + @ParameterizedTest + @BooleanValueSource + void executeWithImageBuildCacheExecutesPhases(boolean trustBuilder) throws Exception { + given(this.docker.container().create(any(), isNull())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().create(any(), isNull(), any())).willAnswer(answerWithGeneratedContainerId()); + given(this.docker.container().wait(any())).willReturn(ContainerStatus.of(0, null)); + BuildRequest request = getTestRequest(trustBuilder).withBuildCache(Cache.image("my-cache-image")); + createLifecycle(request).execute(); + if (trustBuilder) { + assertPhaseWasRun("creator", withExpectedConfig("lifecycle-creator-image-cache.json")); + } + else { + assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer-image-cache.json")); + assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json")); + assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer-image-cache.json")); + assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json")); + assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter-image-cache.json")); + } + assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'"); + } + @ParameterizedTest @BooleanValueSource void executeWithCreatedDateExecutesPhases(boolean trustBuilder) throws Exception { diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java index 42d06b41f148..0771fed79281 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/build/PhaseTests.java @@ -102,6 +102,17 @@ void applyWhenWithArgsUpdatesConfigurationWithArguments() { then(update).shouldHaveNoMoreInteractions(); } + @Test + void applyWhenWithImageBuildCacheUpdatesConfigurationWithCacheImage() { + Phase phase = new Phase("test", false); + phase.withBuildCache("my-cache-image"); + Update update = mock(Update.class); + phase.apply(update); + then(update).should().withCommand("/cnb/lifecycle/test", "-cache-image", "my-cache-image"); + then(update).should().withLabel("author", "spring-boot"); + then(update).shouldHaveNoMoreInteractions(); + } + @Test void applyWhenWithBindsUpdatesConfigurationWithBinds() { Phase phase = new Phase("test", false); diff --git a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java index 84a14ddd7f5b..6a8b8251c5df 100644 --- a/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java +++ b/buildpack/spring-boot-buildpack-platform/src/test/java/org/springframework/boot/buildpack/platform/docker/configuration/CredentialHelperTests.java @@ -106,8 +106,11 @@ void getWhenExecutableDoesNotExistErrorThrowsException() { .satisfies((ex) -> { if (Platform.isMac()) { assertThat(ex.getMessage()).doesNotContain("/usr/local/bin/"); - assertThat(ex.getSuppressed()).allSatisfy((suppressed) -> assertThat(suppressed) - .hasMessageContaining("/usr/local/bin/" + executable)); + assertThat(ex.getSuppressed()).satisfiesExactlyInAnyOrder( + (suppressed) -> assertThat(suppressed) + .hasMessageContaining("/opt/homebrew/bin/" + executable), + (suppressed) -> assertThat(suppressed) + .hasMessageContaining("/usr/local/bin/" + executable)); } }); } diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json new file mode 100644 index 000000000000..e18ed997905c --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-analyzer-image-cache.json @@ -0,0 +1,33 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/analyzer", + "-daemon", + "-cache-image", + "my-cache-image", + "-launch-cache", + "/launch-cache", + "-layers", + "/layers", + "-run-image", + "docker.io/cloudfoundry/run:latest", + "docker.io/library/my-application:latest" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "/var/run/docker.sock:/var/run/docker.sock", + "pack-cache-b35197ac41ea.launch:/launch-cache", + "pack-layers-aaaaaaaaaa:/layers" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json new file mode 100644 index 000000000000..babbd120c8ef --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-creator-image-cache.json @@ -0,0 +1,38 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/creator", + "-app", + "/workspace", + "-platform", + "/platform", + "-run-image", + "docker.io/cloudfoundry/run:latest", + "-layers", + "/layers", + "-cache-image", + "my-cache-image", + "-launch-cache", + "/launch-cache", + "-daemon", + "docker.io/library/my-application:latest" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "pack-app-aaaaaaaaaa:/workspace", + "pack-layers-aaaaaaaaaa:/layers", + "pack-cache-b35197ac41ea.launch:/launch-cache", + "/var/run/docker.sock:/var/run/docker.sock" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json new file mode 100644 index 000000000000..34a4f76b2cad --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-exporter-image-cache.json @@ -0,0 +1,34 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/exporter", + "-daemon", + "-app", + "/workspace", + "-cache-image", + "my-cache-image", + "-launch-cache", + "/launch-cache", + "-layers", + "/layers", + "docker.io/library/my-application:latest" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "/var/run/docker.sock:/var/run/docker.sock", + "pack-app-aaaaaaaaaa:/workspace", + "pack-cache-b35197ac41ea.launch:/launch-cache", + "pack-layers-aaaaaaaaaa:/layers" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} diff --git a/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json new file mode 100644 index 000000000000..a4e74529206a --- /dev/null +++ b/buildpack/spring-boot-buildpack-platform/src/test/resources/org/springframework/boot/buildpack/platform/build/lifecycle-restorer-image-cache.json @@ -0,0 +1,27 @@ +{ + "User": "root", + "Image": "pack.local/ephemeral-builder", + "Cmd": [ + "/cnb/lifecycle/restorer", + "-daemon", + "-cache-image", + "my-cache-image", + "-layers", + "/layers" + ], + "Env": [ + "CNB_PLATFORM_API=0.8" + ], + "Labels": { + "author": "spring-boot" + }, + "HostConfig": { + "Binds": [ + "/var/run/docker.sock:/var/run/docker.sock", + "pack-layers-aaaaaaaaaa:/layers" + ], + "SecurityOpt" : [ + "label=disable" + ] + } +} diff --git a/config/checkstyle/import-control.xml b/config/checkstyle/import-control.xml index 2d591aaaa220..87f3caee62b2 100644 --- a/config/checkstyle/import-control.xml +++ b/config/checkstyle/import-control.xml @@ -6,6 +6,9 @@ + + + @@ -59,7 +62,7 @@ - + diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle index 046f7e126ac9..af31a619c7d8 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/build.gradle @@ -14,21 +14,16 @@ * limitations under the License. */ +import groovy.xml.XmlSlurper + + + plugins { id "java" } description = "Spring Boot Configuration Metadata Changelog Generator" -configurations { - oldMetadata { - transitive = false - } - newMetadata { - transitive = false - } -} - dependencies { implementation(enforcedPlatform(project(":platform:spring-boot-dependencies"))) implementation(project(":configuration-metadata:spring-boot-configuration-metadata")) @@ -42,156 +37,40 @@ nullability { requireExplicitNullMarking = false } -def dependenciesOf(String version) { - if (version.startsWith("4.0")) { - return [ - "spring-boot", - "spring-boot-activemq", - "spring-boot-actuator", - "spring-boot-actuator-autoconfigure", - "spring-boot-amqp", - "spring-boot-artemis", - "spring-boot-autoconfigure", - "spring-boot-batch", - "spring-boot-batch-jdbc", - "spring-boot-cache", - "spring-boot-cassandra", - "spring-boot-couchbase", - "spring-boot-data-cassandra", - "spring-boot-data-commons", - "spring-boot-data-couchbase", - "spring-boot-data-elasticsearch", - "spring-boot-data-jdbc", - "spring-boot-data-jpa", - "spring-boot-data-ldap", - "spring-boot-data-mongodb", - "spring-boot-data-neo4j", - "spring-boot-data-r2dbc", - "spring-boot-data-redis", - "spring-boot-data-rest", - "spring-boot-devtools", - "spring-boot-docker-compose", - "spring-boot-elasticsearch", - "spring-boot-flyway", - "spring-boot-freemarker", - "spring-boot-graphql", - "spring-boot-groovy-templates", - "spring-boot-gson", - "spring-boot-h2console", - "spring-boot-hateoas", - "spring-boot-hazelcast", - "spring-boot-health", - "spring-boot-hibernate", - "spring-boot-http-client", - "spring-boot-http-codec", - "spring-boot-http-converter", - "spring-boot-integration", - "spring-boot-jackson", - "spring-boot-jackson2", - "spring-boot-jdbc", - "spring-boot-jdbc-test", - "spring-boot-jersey", - "spring-boot-jetty", - "spring-boot-jms", - "spring-boot-jooq", - "spring-boot-jpa", - "spring-boot-kafka", - "spring-boot-ldap", - "spring-boot-liquibase", - "spring-boot-mail", - "spring-boot-micrometer-metrics", - "spring-boot-micrometer-metrics-test", - "spring-boot-micrometer-observation", - "spring-boot-micrometer-tracing", - "spring-boot-micrometer-tracing-brave", - "spring-boot-micrometer-tracing-opentelemetry", - "spring-boot-micrometer-tracing-test", - "spring-boot-mongodb", - "spring-boot-mustache", - "spring-boot-neo4j", - "spring-boot-netty", - "spring-boot-opentelemetry", - "spring-boot-persistence", - "spring-boot-pulsar", - "spring-boot-quartz", - "spring-boot-r2dbc", - "spring-boot-reactor", - "spring-boot-reactor-netty", - "spring-boot-restclient", - "spring-boot-restclient-test", - "spring-boot-restdocs", - "spring-boot-rsocket", - "spring-boot-security", - "spring-boot-security-oauth2-authorization-server", - "spring-boot-security-oauth2-client", - "spring-boot-security-oauth2-resource-server", - "spring-boot-security-saml2", - "spring-boot-sendgrid", - "spring-boot-servlet", - "spring-boot-session", - "spring-boot-session-data-redis", - "spring-boot-session-jdbc", - "spring-boot-sql", - "spring-boot-test-autoconfigure", - "spring-boot-testcontainers", - "spring-boot-thymeleaf", - "spring-boot-tomcat", - "spring-boot-transaction", - "spring-boot-validation", - "spring-boot-web-server", - "spring-boot-webclient", - "spring-boot-webflux", - "spring-boot-webflux-test", - "spring-boot-webmvc", - "spring-boot-webmvc-test", - "spring-boot-webservices", - "spring-boot-webservices-test", - "spring-boot-zipkin" - ] - } - return [ - "spring-boot", - "spring-boot-actuator", - "spring-boot-actuator-autoconfigure", - "spring-boot-autoconfigure", - "spring-boot-devtools", - "spring-boot-docker-compose", - "spring-boot-test-autoconfigure", - "spring-boot-testcontainers" - ] -} - if (project.hasProperty("oldVersion") && project.hasProperty("newVersion")) { - dependencies { - dependenciesOf(oldVersion).each { - oldMetadata("org.springframework.boot:$it:$oldVersion") + configurations { + oldMetadata { + transitive = false } - dependenciesOf(newVersion).each { - newMetadata("org.springframework.boot:$it:$newVersion") + newMetadata { + transitive = false } } - + configurations.oldMetadata.dependencies.addAllLater(bootArtifactsFromBom(oldVersion)) + configurations.newMetadata.dependencies.addAllLater(bootArtifactsFromBom(newVersion)) def prepareOldMetadata = tasks.register("prepareOldMetadata", Sync) { from(configurations.oldMetadata) - if (project.hasProperty("oldVersion")) { - destinationDir = project.file("build/configuration-metadata-diff/$oldVersion") - } + destinationDir = project.file("build/configuration-metadata-diff/$oldVersion") } - def prepareNewMetadata = tasks.register("prepareNewMetadata", Sync) { from(configurations.newMetadata) - if (project.hasProperty("newVersion")) { - destinationDir = project.file("build/configuration-metadata-diff/$newVersion") - } + destinationDir = project.file("build/configuration-metadata-diff/$newVersion") } - tasks.register("generate", JavaExec) { inputs.files(prepareOldMetadata, prepareNewMetadata) outputs.file(project.file("build/configuration-metadata-changelog.adoc")) classpath = sourceSets.main.runtimeClasspath mainClass = 'org.springframework.boot.configurationmetadata.changelog.ChangelogGenerator' - if (project.hasProperty("oldVersion") && project.hasProperty("newVersion")) { - args = [project.file("build/configuration-metadata-diff/$oldVersion"), project.file("build/configuration-metadata-diff/$newVersion"), project.file("build/configuration-metadata-changelog.adoc")] - } + args = [project.file("build/configuration-metadata-diff/$oldVersion"), project.file("build/configuration-metadata-diff/$newVersion"), project.file("build/configuration-metadata-changelog.adoc")] } } + +def bootArtifactsFromBom(def version) { + project.provider { + def bomDependency = project.dependencies.create("org.springframework.boot:spring-boot-dependencies:$version@pom") + def bom = new XmlSlurper().parse(configurations.detachedConfiguration(bomDependency).singleFile) + return bom.dependencyManagement.dependencies.dependency + .findAll { it.groupId == "org.springframework.boot" } + .collect { project.dependencies.create("org.springframework.boot:${it.artifactId}:$version") } + } +} diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java index 9aa98bc1be50..9aad9208d7d7 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/ChangelogWriter.java @@ -68,6 +68,8 @@ void write(Changelog changelog) { String newVersionNumber = changelog.newVersionNumber(); Map> differencesByType = collateByType(changelog); write("Configuration property changes between `%s` and `%s`%n", oldVersionNumber, newVersionNumber); + write("%n%n%n== Default Changed in %s%n%n", newVersionNumber); + writeDefaultChanged(differencesByType.get(DifferenceType.DEFAULT_CHANGED)); write("%n%n%n== Deprecated in %s%n%n", newVersionNumber); writeDeprecated(differencesByType.get(DifferenceType.DEPRECATED)); write("%n%n%n== Added in %s%n%n", newVersionNumber); @@ -98,6 +100,17 @@ private void writeDeprecated(Difference difference) { writeDeprecatedPropertyRow(difference.newProperty()); } + private void writeDefaultChanged(List differences) { + List rows = sortProperties(differences, Difference::newProperty); + writeTable("| Key | Old Default | New Default", rows, this::writeDefaultChanged); + } + + private void writeDefaultChanged(Difference difference) { + writeCell(monospace(difference.newProperty().getId())); + writeCell(monospace(asString(difference.oldProperty().getDefaultValue()))); + writeCell(monospace(asString(difference.newProperty().getDefaultValue()))); + } + private void writeAdded(List differences) { List rows = sortProperties(differences, Difference::newProperty); writeTable("| Key | Default value | Description", rows, this::writeAdded); diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java index 733a5755d93f..a0532b289d9b 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/Difference.java @@ -16,6 +16,8 @@ package org.springframework.boot.configurationmetadata.changelog; +import java.util.Objects; + import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; import org.springframework.boot.configurationmetadata.Deprecation.Level; @@ -49,6 +51,9 @@ static Difference compute(ConfigurationMetadataProperty oldProperty, Configurati && newProperty.isDeprecated() && newProperty.getDeprecation().getLevel() == Level.ERROR) { return new Difference(DifferenceType.DELETED, oldProperty, newProperty); } + if (!Objects.deepEquals(oldProperty.getDefaultValue(), newProperty.getDefaultValue())) { + return new Difference(DifferenceType.DEFAULT_CHANGED, oldProperty, newProperty); + } return null; } diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java index d8bbe93560b9..46246c99296f 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/main/java/org/springframework/boot/configurationmetadata/changelog/DifferenceType.java @@ -37,6 +37,12 @@ enum DifferenceType { /** * The entry has been deleted. */ - DELETED + DELETED, + + /** + * The entry's default value has changed. The entry exists in both the previous and + * current versions, but its default value is different. + */ + DEFAULT_CHANGED } diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java index 100f5a99f27d..487d78ccdfd5 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/ChangelogTests.java @@ -39,7 +39,7 @@ void diffContainsDifferencesBetweenLeftAndRightInputs() { assertThat(differences).isNotNull(); assertThat(differences.oldVersionNumber()).isEqualTo("1.0"); assertThat(differences.newVersionNumber()).isEqualTo("2.0"); - assertThat(differences.differences()).hasSize(7); + assertThat(differences.differences()).hasSize(9); List added = differences.differences() .stream() .filter((difference) -> difference.type() == DifferenceType.ADDED) @@ -65,12 +65,20 @@ void diffContainsDifferencesBetweenLeftAndRightInputs() { assertThat(deprecated).hasSize(1); assertProperty(deprecated.get(0).oldProperty(), "test.deprecate", String.class, "wrong"); assertProperty(deprecated.get(0).newProperty(), "test.deprecate", String.class, "wrong"); + List defaultChanged = differences.differences() + .stream() + .filter((difference) -> difference.type() == DifferenceType.DEFAULT_CHANGED) + .toList(); + assertThat(defaultChanged).hasSize(2) + .anySatisfy((entry) -> assertProperty(entry.newProperty(), "test.default.array.change", String[].class, + new String[] { "b", "a" })) + .anySatisfy((entry) -> assertProperty(entry.newProperty(), "test.default.change", String.class, "new")); } private void assertProperty(ConfigurationMetadataProperty property, String id, Class type, Object defaultValue) { assertThat(property).isNotNull(); assertThat(property.getId()).isEqualTo(id); - assertThat(property.getType()).isEqualTo(type.getName()); + assertThat(property.getType()).isEqualTo(type.getCanonicalName()); assertThat(property.getDefaultValue()).isEqualTo(defaultValue); } diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java new file mode 100644 index 000000000000..fbf92da367f4 --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/java/org/springframework/boot/configurationmetadata/changelog/DifferenceTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2012-present 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.configurationmetadata.changelog; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.configurationmetadata.ConfigurationMetadataProperty; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link Difference}. + * + * @author Stephane Nicoll + */ +class DifferenceTests { + + private static ConfigurationMetadataProperty createProperty(String id, Object defaultValue) { + ConfigurationMetadataProperty property = new ConfigurationMetadataProperty(); + property.setId(id); + property.setDefaultValue(defaultValue); + return property; + } + + @Nested + class DefaultChangedTests { + + @Test + void sameValueComputesNoDifference() { + Difference difference = Difference.compute(createProperty("test.id", "test"), + createProperty("test.id", "test")); + assertThat(difference).isNull(); + } + + @Test + void bothNullComputesNoDifference() { + Difference difference = Difference.compute(createProperty("test.id", null), + createProperty("test.id", null)); + assertThat(difference).isNull(); + } + + @Test + void nullThenNotNullDefaultChanged() { + Difference difference = Difference.compute(createProperty("test.id", null), + createProperty("test.id", "test")); + assertThat(difference).isNotNull(); + assertThat(difference.type()).isEqualTo(DifferenceType.DEFAULT_CHANGED); + } + + @Test + void notNullThenNullDefaultChanged() { + Difference difference = Difference.compute(createProperty("test.id", "test"), + createProperty("test.id", null)); + assertThat(difference).isNotNull(); + assertThat(difference.type()).isEqualTo(DifferenceType.DEFAULT_CHANGED); + } + + @Test + void arrayEqualsComputesNoDifference() { + Difference difference = Difference.compute(createProperty("test.id", new Object[] { "one", "two" }), + createProperty("test.id", new Object[] { "one", "two" })); + assertThat(difference).isNull(); + } + + @Test + void arrayOrderChangedComputesDefaultChanged() { + Difference difference = Difference.compute(createProperty("test.id", new Object[] { "one", "two" }), + createProperty("test.id", new Object[] { "two", "one" })); + assertThat(difference).isNotNull(); + assertThat(difference.type()).isEqualTo(DifferenceType.DEFAULT_CHANGED); + } + + @Test + void arrayAdditionalValueComputesDefaultChanged() { + Difference difference = Difference.compute(createProperty("test.id", new Object[] { "one", "two" }), + createProperty("test.id", new Object[] { "one", "two", "three" })); + assertThat(difference).isNotNull(); + assertThat(difference.type()).isEqualTo(DifferenceType.DEFAULT_CHANGED); + } + + } + +} diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json index 6f6ab8468453..e28ce4affd6c 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-1.0.json @@ -6,6 +6,18 @@ "description": "Test equality.", "defaultValue": "test" }, + { + "name": "test.default.change", + "type": "java.lang.String", + "description": "Test default value change.", + "defaultValue": "old" + }, + { + "name": "test.default.array.change", + "type": "java.lang.String[]", + "description": "Test default array value change.", + "defaultValue": ["a", "b"] + }, { "name": "test.replace", "type": "java.lang.String", diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json index e650460140de..48923d76ec42 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample-2.0.json @@ -6,6 +6,18 @@ "description": "Test add.", "defaultValue": "new" }, + { + "name": "test.default.change", + "type": "java.lang.String", + "description": "Test default value change.", + "defaultValue": "new" + }, + { + "name": "test.default.array.change", + "type": "java.lang.String[]", + "description": "Test default array value change.", + "defaultValue": ["b", "a"] + }, { "name": "test.replace", "type": "java.lang.String", diff --git a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc index 908f48e73c53..7a0f17b8ada5 100644 --- a/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc +++ b/configuration-metadata/spring-boot-configuration-metadata-changelog-generator/src/test/resources/sample.adoc @@ -2,6 +2,22 @@ Configuration property changes between `1.0` and `2.0` +== Default Changed in 2.0 + +|====================== +| Key | Old Default | New Default + +| `test.default.array.change` +| `a, b` +| `b, a` + +| `test.default.change` +| `old` +| `new` +|====================== + + + == Deprecated in 2.0 |====================== diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationPropertiesSourceResolver.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationPropertiesSourceResolver.java index d13d386d07d5..db5d8a643d9b 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationPropertiesSourceResolver.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConfigurationPropertiesSourceResolver.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.tools.FileObject; import javax.tools.StandardLocation; @@ -146,6 +147,11 @@ static class SourcePropertyDescriptor extends PropertyDescriptor { this.sourceItemHint = sourceItemHint; } + @Override + protected Element getSourceElement() { + return this.delegate.getSourceElement(); + } + @Override protected ItemHint resolveItemHint(String prefix, MetadataGenerationEnvironment environment) { return (this.sourceItemHint != null) ? this.sourceItemHint.applyPrefix(prefix) diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java index e49ef48a9a1c..f6fdf3bbaa39 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptor.java @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -42,6 +43,11 @@ class ConstructorParameterPropertyDescriptor extends ParameterPropertyDescriptor this.field = field; } + @Override + protected Element getSourceElement() { + return this.field; + } + @Override protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) { return resolveItemDeprecation(environment, getGetter(), this.setter, this.field); diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java index 05120a645eec..bc2df607b56a 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptor.java @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -45,6 +46,11 @@ class JavaBeanPropertyDescriptor extends PropertyDescriptor { this.factoryMethod = factoryMethod; } + @Override + protected Element getSourceElement() { + return getGetter(); + } + ExecutableElement getSetter() { return this.setter; } diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java index 565390d97217..09d8408be2c1 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptor.java @@ -19,6 +19,7 @@ import java.util.Map; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; @@ -59,6 +60,11 @@ class LombokPropertyDescriptor extends PropertyDescriptor { this.setter = setter; } + @Override + protected Element getSourceElement() { + return getField(); + } + VariableElement getField() { return this.field; } diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java index 6bf5cebbac91..5fe2e7883f41 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/ParameterPropertyDescriptor.java @@ -47,6 +47,11 @@ abstract class ParameterPropertyDescriptor extends PropertyDescriptor { } + @Override + protected Element getSourceElement() { + return getParameter(); + } + final VariableElement getParameter() { return this.parameter; } diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java index 75531fa9aeda..97abe0dd2237 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptor.java @@ -91,6 +91,12 @@ protected final ExecutableElement getGetter() { return this.getter; } + /** + * Return the {@link Element} that primarily defines the property. + * @return the source element + */ + protected abstract Element getSourceElement(); + /** * Resolve the {@link ItemMetadata} for this property. * @param prefix the property prefix diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java index ba4aa6ec936c..b90369a317ee 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/PropertyDescriptorResolver.java @@ -20,6 +20,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Stream; import javax.lang.model.element.AnnotationMirror; @@ -31,6 +32,7 @@ import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; +import javax.tools.Diagnostic.Kind; import org.springframework.boot.configurationprocessor.ConfigurationPropertiesSourceResolver.SourceMetadata; @@ -149,15 +151,30 @@ private ExecutableElement findMatchingGetter(TypeElementMembers members, List candidates, PropertyDescriptor descriptor) { - if (!candidates.containsKey(descriptor.getName()) && isCandidate(descriptor)) { + if (!isCandidate(descriptor)) { + return; + } + PropertyDescriptor existing = candidates.get(descriptor.getName()); + if (existing == null) { candidates.put(descriptor.getName(), descriptor); } + else if (isDistinctDescriptor(existing, descriptor)) { + this.environment.getMessager() + .printMessage(Kind.ERROR, + "Property name '%s' maps to distinct properties in type %s".formatted(descriptor.getName(), + this.environment.getTypeUtils().getQualifiedName(descriptor.getDeclaringElement()))); + } } private boolean isCandidate(PropertyDescriptor descriptor) { return descriptor.isProperty(this.environment) || descriptor.isNested(this.environment); } + private boolean isDistinctDescriptor(PropertyDescriptor existing, PropertyDescriptor candidate) { + return Objects.equals(existing.getClass(), candidate.getClass()) + && !Objects.equals(existing.getSourceElement(), candidate.getSourceElement()); + } + /** * Wrapper around a {@link TypeElement} that could be bound. */ diff --git a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java index d0d33148fa63..f17f8e1f4036 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/main/java/org/springframework/boot/configurationprocessor/RecordParameterPropertyDescriptor.java @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.RecordComponentElement; import javax.lang.model.element.TypeElement; @@ -41,6 +42,11 @@ class RecordParameterPropertyDescriptor extends ParameterPropertyDescriptor { this.recordComponent = recordComponent; } + @Override + protected Element getSourceElement() { + return this.recordComponent; + } + @Override protected ItemDeprecation resolveItemDeprecation(MetadataGenerationEnvironment environment) { return resolveItemDeprecation(environment, getGetter()); diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java index 7652240097da..a390e2ebd333 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/ConstructorParameterPropertyDescriptorTests.java @@ -19,6 +19,7 @@ import java.util.Arrays; import java.util.List; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -55,6 +56,17 @@ void constructorParameterSimpleProperty() { }); } + @Test + void getSourceElementReturnsBackingField() { + process(ImmutableSimpleProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv.getRootElement(ImmutableSimpleProperties.class); + ConstructorParameterPropertyDescriptor property = createPropertyDescriptor(ownerElement, "theName"); + assertThat(property.getSourceElement().getKind()).isEqualTo(ElementKind.FIELD); + assertThat(property.getSourceElement().getSimpleName()).hasToString("theName"); + assertThat(property.getSourceElement()).isNotSameAs(property.getGetter()); + }); + } + @Test void constructorParameterNestedPropertySameClass() { process(ImmutableInnerClassProperties.class, (roundEnv, metadataEnv) -> { diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java index e6ca9d5e2272..74bc8c2a560e 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/JavaBeanPropertyDescriptorTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -50,6 +51,16 @@ void javaBeanSimpleProperty() { }); } + @Test + void getSourceElementReturnsPublicGetter() { + process(SimpleTypeProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv.getRootElement(SimpleTypeProperties.class); + JavaBeanPropertyDescriptor property = createPropertyDescriptor(ownerElement, "myString"); + assertThat(property.getSourceElement().getKind()).isEqualTo(ElementKind.METHOD); + assertThat(property.getSourceElement()).isSameAs(property.getGetter()); + }); + } + @Test void javaBeanCollectionProperty() { process(SimpleCollectionProperties.class, (roundEnv, metadataEnv) -> { diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java index ef5fba054596..6c2b07abd33f 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/LombokPropertyDescriptorTests.java @@ -16,6 +16,7 @@ package org.springframework.boot.configurationprocessor; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; @@ -53,6 +54,16 @@ void lombokSimpleProperty() { }); } + @Test + void getSourceElementReturnsField() { + process(LombokSimpleProperties.class, (roundEnv, metadataEnv) -> { + TypeElement ownerElement = roundEnv.getRootElement(LombokSimpleProperties.class); + LombokPropertyDescriptor property = createPropertyDescriptor(ownerElement, "name"); + assertThat(property.getSourceElement().getKind()).isEqualTo(ElementKind.FIELD); + assertThat(property.getSourceElement()).isSameAs(property.getField()); + }); + } + @Test void lombokCollectionProperty() { process(LombokSimpleProperties.class, (roundEnv, metadataEnv) -> { diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/NameAnnotationPropertiesTests.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/NameAnnotationPropertiesTests.java index 440a876252a7..d0dd1e47ef32 100644 --- a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/NameAnnotationPropertiesTests.java +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationprocessor/NameAnnotationPropertiesTests.java @@ -21,11 +21,15 @@ import org.springframework.boot.configurationprocessor.metadata.ConfigurationMetadata; import org.springframework.boot.configurationprocessor.metadata.Metadata; import org.springframework.boot.configurationsample.name.ConstructorParameterNameAnnotationProperties; +import org.springframework.boot.configurationsample.name.DuplicateNameConstructorParameterProperties; +import org.springframework.boot.configurationsample.name.DuplicateNameJavaBeanProperties; import org.springframework.boot.configurationsample.name.JavaBeanNameAnnotationProperties; import org.springframework.boot.configurationsample.name.LombokNameAnnotationProperties; import org.springframework.boot.configurationsample.name.RecordComponentNameAnnotationProperties; +import org.springframework.core.test.tools.CompilationException; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Metadata generation tests for using {@code @Name}. @@ -87,4 +91,22 @@ void lombokNameAnnotationProperties() { .withDefaultValue(true)); } + @Test + void duplicateNameOnJavaBeanPropertiesFailsCompilation() { + assertThatExceptionOfType(CompilationException.class) + .isThrownBy(() -> compile(DuplicateNameJavaBeanProperties.class)) + .withMessageContaining("Unable to compile source") + .withMessageContaining("Property name 'aaa' maps to distinct properties in type ") + .withMessageContaining(DuplicateNameJavaBeanProperties.class.getName()); + } + + @Test + void duplicateNameOnConstructorParametersFailsCompilation() { + assertThatExceptionOfType(CompilationException.class) + .isThrownBy(() -> compile(DuplicateNameConstructorParameterProperties.class)) + .withMessageContaining("Unable to compile source") + .withMessageContaining("Property name 'aaa' maps to distinct properties in type ") + .withMessageContaining(DuplicateNameConstructorParameterProperties.class.getName()); + } + } diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameConstructorParameterProperties.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameConstructorParameterProperties.java new file mode 100644 index 000000000000..1a4df523230a --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameConstructorParameterProperties.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-present 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.configurationsample.name; + +import org.springframework.boot.configurationsample.TestConfigurationProperties; +import org.springframework.boot.configurationsample.TestName; + +/** + * Sample where two constructor parameters use the same {@code @Name} value (invalid). + * + * @author Stephane Nicoll + */ +@TestConfigurationProperties("duplicate") +public class DuplicateNameConstructorParameterProperties { + + private final String company; + + private final String name; + + public DuplicateNameConstructorParameterProperties(@TestName("aaa") String company, @TestName("aaa") String name) { + this.company = company; + this.name = name; + } + + public String getCompany() { + return this.company; + } + + public String getName() { + return this.name; + } + +} diff --git a/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameJavaBeanProperties.java b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameJavaBeanProperties.java new file mode 100644 index 000000000000..c8588004834b --- /dev/null +++ b/configuration-metadata/spring-boot-configuration-processor/src/test/java/org/springframework/boot/configurationsample/name/DuplicateNameJavaBeanProperties.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-present 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.configurationsample.name; + +import org.springframework.boot.configurationsample.TestConfigurationProperties; +import org.springframework.boot.configurationsample.TestName; + +/** + * Sample where two JavaBean properties use the same {@code @Name} value (invalid). + * + * @author Stephane Nicoll + */ +@TestConfigurationProperties("duplicate") +public class DuplicateNameJavaBeanProperties { + + @TestName("aaa") + private String company; + + @TestName("aaa") + private String name; + + public String getCompany() { + return this.company; + } + + public void setCompany(String company) { + this.company = company; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/core/spring-boot-autoconfigure/build.gradle b/core/spring-boot-autoconfigure/build.gradle index 7d6972d6b132..100b29d8006d 100644 --- a/core/spring-boot-autoconfigure/build.gradle +++ b/core/spring-boot-autoconfigure/build.gradle @@ -30,6 +30,7 @@ dependencies { optional("com.github.ben-manes.caffeine:caffeine") optional("org.aspectj:aspectjweaver") + optional("io.micrometer:context-propagation") optional("jakarta.servlet:jakarta.servlet-api") optional("javax.money:money-api") optional("org.springframework:spring-web") diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/DefaultTaskSchedulerConfiguration.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/DefaultTaskSchedulerConfiguration.java new file mode 100644 index 000000000000..ba13fb6a9970 --- /dev/null +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/DefaultTaskSchedulerConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-present 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.autoconfigure.task; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; +import org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder; +import org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder; +import org.springframework.boot.thread.Threading; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; +import org.springframework.scheduling.config.TaskSchedulerRouter; + +/** + * Configuration that can be imported to expose a standard {@link TaskScheduler} if the + * user has not enabled task scheduling explicitly. A {@link SimpleAsyncTaskScheduler} is + * exposed if the user enables virtual threads via + * {@code spring.threads.virtual.enabled=true}, otherwise {@link ThreadPoolTaskScheduler}. + *

+ * Configurations importing this one should be ordered after + * {@link TaskSchedulingAutoConfiguration}. + * + * @author Phillip Webb + * @since 4.1.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(name = DefaultTaskSchedulerConfiguration.DEFAULT_TASK_SCHEDULER_BEAN_NAME) +public class DefaultTaskSchedulerConfiguration { + + /** + * The bean name of the default task scheduler. + */ + public static final String DEFAULT_TASK_SCHEDULER_BEAN_NAME = TaskSchedulerRouter.DEFAULT_TASK_SCHEDULER_BEAN_NAME; + + @Bean(name = DEFAULT_TASK_SCHEDULER_BEAN_NAME) + @ConditionalOnBean(ThreadPoolTaskSchedulerBuilder.class) + @ConditionalOnThreading(Threading.PLATFORM) + ThreadPoolTaskScheduler taskScheduler(ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder) { + return threadPoolTaskSchedulerBuilder.build(); + } + + @Bean(name = DEFAULT_TASK_SCHEDULER_BEAN_NAME) + @ConditionalOnBean(SimpleAsyncTaskSchedulerBuilder.class) + @ConditionalOnThreading(Threading.VIRTUAL) + SimpleAsyncTaskScheduler taskSchedulerVirtualThreads( + SimpleAsyncTaskSchedulerBuilder simpleAsyncTaskSchedulerBuilder) { + return simpleAsyncTaskSchedulerBuilder.build(); + } + +} diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java index ab6eb21cf478..7e17fd524d9a 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfiguration.java @@ -35,7 +35,8 @@ @ConditionalOnClass(ThreadPoolTaskExecutor.class) @AutoConfiguration @EnableConfigurationProperties(TaskExecutionProperties.class) -@Import({ TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class, +@Import({ TaskExecutorConfigurations.TaskExecutorContextPropagationConfiguration.class, + TaskExecutorConfigurations.ThreadPoolTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.SimpleAsyncTaskExecutorBuilderConfiguration.class, TaskExecutorConfigurations.TaskExecutorConfiguration.class, TaskExecutorConfigurations.BootstrapExecutorConfiguration.class }) diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java index 144fbb60e67c..e2dbfcab0612 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutionProperties.java @@ -44,6 +44,11 @@ public class TaskExecutionProperties { */ private Mode mode = Mode.AUTO; + /** + * Whether to propagate the current context to task executions. + */ + private boolean propagateContext; + /** * Prefix to use for the names of newly created threads. */ @@ -69,6 +74,14 @@ public void setMode(Mode mode) { this.mode = mode; } + public boolean getPropagateContext() { + return this.propagateContext; + } + + public void setPropagateContext(boolean propagateContext) { + this.propagateContext = propagateContext; + } + public String getThreadNamePrefix() { return this.threadNamePrefix; } diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java index a519c65374a9..3909065231b1 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskExecutorConfigurations.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.concurrent.Executor; +import io.micrometer.context.ContextSnapshot; import org.jspecify.annotations.Nullable; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; @@ -29,6 +30,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading; @@ -47,6 +49,7 @@ import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.support.CompositeTaskDecorator; +import org.springframework.core.task.support.ContextPropagatingTaskDecorator; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -68,6 +71,18 @@ class TaskExecutorConfigurations { return (!taskDecorators.isEmpty()) ? new CompositeTaskDecorator(taskDecorators) : null; } + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ContextSnapshot.class) + static class TaskExecutorContextPropagationConfiguration { + + @Bean + @ConditionalOnProperty(name = "spring.task.execution.propagate-context", havingValue = "true") + ContextPropagatingTaskDecorator contextPropagatingTaskDecorator() { + return new ContextPropagatingTaskDecorator(); + } + + } + @Configuration(proxyBeanMethods = false) @Conditional(OnExecutorCondition.class) @Import({ AsyncConfigurerWrapperConfiguration.class, AsyncConfigurerConfiguration.class }) diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java index b08df1625fb6..d3e31e5d851a 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/task/TaskSchedulingConfigurations.java @@ -32,11 +32,10 @@ import org.springframework.boot.thread.Threading; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.support.CompositeTaskDecorator; import org.springframework.scheduling.TaskScheduler; -import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.TaskManagementConfigUtils; /** @@ -58,20 +57,9 @@ class TaskSchedulingConfigurations { @Configuration(proxyBeanMethods = false) @ConditionalOnBean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) @ConditionalOnMissingBean({ TaskScheduler.class, ScheduledExecutorService.class }) + @Import(DefaultTaskSchedulerConfiguration.class) static class TaskSchedulerConfiguration { - @Bean(name = "taskScheduler") - @ConditionalOnThreading(Threading.VIRTUAL) - SimpleAsyncTaskScheduler taskSchedulerVirtualThreads(SimpleAsyncTaskSchedulerBuilder builder) { - return builder.build(); - } - - @Bean - @ConditionalOnThreading(Threading.PLATFORM) - ThreadPoolTaskScheduler taskScheduler(ThreadPoolTaskSchedulerBuilder threadPoolTaskSchedulerBuilder) { - return threadPoolTaskSchedulerBuilder.build(); - } - } @Configuration(proxyBeanMethods = false) diff --git a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java index 06826ddb9e0c..c33694a5c11b 100644 --- a/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java +++ b/core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/format/WebConversionService.java @@ -31,6 +31,7 @@ import org.springframework.format.number.money.MonetaryAmountFormatter; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.util.ClassUtils; +import org.springframework.util.StringValueResolver; /** * {@link org.springframework.format.support.FormattingConversionService} dedicated to web @@ -53,7 +54,23 @@ public class WebConversionService extends DefaultFormattingConversionService { * @since 2.3.0 */ public WebConversionService(DateTimeFormatters dateTimeFormatters) { - super(false); + this(null, dateTimeFormatters); + } + + /** + * Create a new WebConversionService that configures formatters with the provided + * date, time, and date-time formats, or registers the default if no custom format is + * provided. The given {@code embeddedValueResolver} is used to resolve embedded + * values such as property placeholders in + * {@link org.springframework.format.annotation.DateTimeFormat#pattern()} patterns. + * @param embeddedValueResolver the embedded value resolver to use, or {@code null} + * @param dateTimeFormatters the formatters to use for date, time, and date-time + * formatting + * @since 4.1.0 + */ + public WebConversionService(@Nullable StringValueResolver embeddedValueResolver, + DateTimeFormatters dateTimeFormatters) { + super(embeddedValueResolver, false); if (dateTimeFormatters.isCustomized()) { addFormatters(dateTimeFormatters); } diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java index 40d6fb26e7a6..862452952f77 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/task/TaskExecutionAutoConfigurationTests.java @@ -16,6 +16,10 @@ package org.springframework.boot.autoconfigure.task; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; @@ -24,6 +28,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import io.micrometer.context.ThreadLocalAccessor; import org.assertj.core.api.InstanceOfAssertFactories; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -42,6 +47,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ContextConsumer; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.testsupport.classpath.resources.WithResource; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -50,6 +56,7 @@ import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.support.CompositeTaskDecorator; +import org.springframework.core.task.support.ContextPropagatingTaskDecorator; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncConfigurer; @@ -253,6 +260,35 @@ void simpleAsyncTaskExecutorBuilderUsesVirtualThreadsWhenEnabled() { }); } + @Test + @WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor", + content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests$TestThreadLocalAccessor") + void asyncTaskExecutorShouldNotNotRegisterContextPropagatingTaskDecoratorByDefault() { + this.contextRunner.withUserConfiguration(AsyncConfiguration.class, TestBean.class).run((context) -> { + assertThat(context).doesNotHaveBean(ContextPropagatingTaskDecorator.class); + TestBean bean = context.getBean(TestBean.class); + TestThreadLocalHolder.setValue("from-context"); + String text = bean.echoContext().get(); + assertThat(text).contains("task-").endsWith("null"); + }); + + } + + @Test + @WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor", + content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests$TestThreadLocalAccessor") + void asyncTaskExecutorWhenContextPropagationIsEnabledShouldRegisterBean() { + this.contextRunner.withUserConfiguration(AsyncConfiguration.class, TestBean.class) + .withPropertyValues("spring.task.execution.propagate-context=true") + .run((context) -> { + assertThat(context).hasSingleBean(ContextPropagatingTaskDecorator.class); + TestBean bean = context.getBean(TestBean.class); + TestThreadLocalHolder.setValue("from-context"); + String text = bean.echoContext().get(); + assertThat(text).contains("task-").endsWith("from-context"); + }); + } + @Test void taskExecutorWhenHasCustomTaskExecutorShouldBackOff() { this.contextRunner.withBean("customTaskExecutor", Executor.class, SyncTaskExecutor::new).run((context) -> { @@ -591,6 +627,14 @@ private String virtualThreadName(SimpleAsyncTaskExecutor taskExecutor) throws In return thread.getName(); } + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + @WithResource(name = "META-INF/services/io.micrometer.context.ThreadLocalAccessor", + content = "org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfigurationTests.TestThreadLocalAccessor") + @interface WithThreadLocalAccessor { + + } + @Configuration(proxyBeanMethods = false) static class CustomThreadPoolTaskExecutorBuilderConfig { @@ -622,6 +666,56 @@ Future echo(String text) { return CompletableFuture.completedFuture(Thread.currentThread().getName() + " " + text); } + @Async + Future echoContext() { + return CompletableFuture + .completedFuture(Thread.currentThread().getName() + " " + TestThreadLocalHolder.getValue()); + } + + } + + static class TestThreadLocalHolder { + + private static final ThreadLocal holder = new ThreadLocal<>(); + + static void setValue(String value) { + holder.set(value); + } + + static String getValue() { + return holder.get(); + } + + static void reset() { + holder.remove(); + } + + } + + public static class TestThreadLocalAccessor implements ThreadLocalAccessor { + + static final String KEY = "test.threadlocal"; + + @Override + public Object key() { + return KEY; + } + + @Override + public String getValue() { + return TestThreadLocalHolder.getValue(); + } + + @Override + public void setValue(String value) { + TestThreadLocalHolder.setValue(value); + } + + @Override + public void setValue() { + TestThreadLocalHolder.reset(); + } + } } diff --git a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java index 4b17ace39f3c..800fa7f087f0 100644 --- a/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java +++ b/core/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/format/WebConversionServiceTests.java @@ -29,8 +29,12 @@ import java.util.Calendar; import java.util.Date; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.format.annotation.DateTimeFormat; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -178,10 +182,27 @@ void convertFromStringToDateWithIsoFormatting() { assertThat(calendar.get(Calendar.DAY_OF_MONTH)).isOne(); } + @Test + void embeddedValueResolverIsAppliedToAnnotationPattern() throws Exception { + WebConversionService conversionService = new WebConversionService( + (value) -> value.replace("${my.date.format}", "yyyy-MM-dd"), new DateTimeFormatters()); + TypeDescriptor dateType = new TypeDescriptor(FormattedDate.class.getDeclaredField("date")); + Date date = Date.from(ZonedDateTime.of(2000, 1, 2, 3, 4, 5, 6, ZoneId.systemDefault()).toInstant()); + assertThat(conversionService.convert(date, dateType, TypeDescriptor.valueOf(String.class))) + .isEqualTo("2000-01-02"); + } + private void customDateFormat(Object input) { WebConversionService conversionService = new WebConversionService( new DateTimeFormatters().dateFormat("dd*MM*yyyy")); assertThat(conversionService.convert(input, String.class)).isEqualTo("01*01*2018"); } + static class FormattedDate { + + @DateTimeFormat(pattern = "${my.date.format}") + @Nullable Date date; + + } + } diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java index 19f922763b64..e2ca2e753d79 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DefaultDockerCompose.java @@ -88,6 +88,11 @@ public void stop(Duration timeout, List arguments) { this.cli.run(new DockerCliCommand.ComposeStop(timeout, arguments)); } + @Override + public String logs() { + return this.cli.run(new DockerCliCommand.ComposeLogs()); + } + @Override public boolean hasDefinedServices() { return !this.cli.run(new DockerCliCommand.ComposeConfig()).services().isEmpty(); diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java index 194f2da41adf..1a9fb9d99895 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCli.java @@ -79,8 +79,8 @@ R run(DockerCliCommand dockerCommand) { List command = createCommand(dockerCommand.getType()); command.addAll(dockerCommand.getCommand(this.composeVersion)); Consumer outputConsumer = createOutputConsumer(dockerCommand.getLogLevel()); - String json = this.processRunner.run(outputConsumer, command.toArray(new String[0])); - return dockerCommand.deserialize(json); + String response = this.processRunner.run(outputConsumer, command.toArray(new String[0])); + return dockerCommand.convert(response); } private @Nullable Consumer createOutputConsumer(@Nullable LogLevel logLevel) { diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java index 86fedba694a1..4662295c2c43 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCliCommand.java @@ -77,12 +77,15 @@ List getCommand(ComposeVersion composeVersion) { } @SuppressWarnings("unchecked") - R deserialize(String json) { + R convert(String response) { if (this.responseType == None.class) { return (R) None.INSTANCE; } - return (R) ((!this.listResponse) ? DockerJson.deserialize(json, this.responseType) - : DockerJson.deserializeToList(json, this.responseType)); + if (this.responseType == String.class) { + return (R) response; + } + return (R) ((!this.listResponse) ? DockerJson.deserialize(response, this.responseType) + : DockerJson.deserializeToList(response, this.responseType)); } @Override @@ -251,6 +254,17 @@ private static String[] getCommand(Duration timeout, List arguments) { } + /** + * The {@code docker compose logs} command. + */ + static final class ComposeLogs extends DockerCliCommand { + + ComposeLogs() { + super(Type.DOCKER_COMPOSE, String.class, false, "logs"); + } + + } + /** * Command Types. */ diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java index 4c82f793ef37..619b6c7de7f3 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/DockerCompose.java @@ -105,6 +105,13 @@ public interface DockerCompose { */ void stop(Duration timeout, List arguments); + /** + * Run {@code docker compose logs} to view Docker Compose's recent logs. + * @return the recent logs + * @since 4.1.0 + */ + String logs(); + /** * Return if services have been defined in the {@link DockerComposeFile} for the * active profiles. diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java index b0491330f591..2aaac547822a 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/core/ProcessRunner.java @@ -44,6 +44,10 @@ class ProcessRunner { private static final String USR_LOCAL_BIN = "/usr/local/bin"; + private static final String OPT_HOMEBREW_BIN = "/opt/homebrew/bin"; + + private static final String[] MAC_OS_BIN_DIRECTORIES = { OPT_HOMEBREW_BIN, USR_LOCAL_BIN }; + private static final boolean MAC_OS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("mac"); private static final Log logger = LogFactory.getLog(ProcessRunner.class); @@ -108,11 +112,22 @@ private Process startProcess(String[] command) { } catch (IOException ex) { String path = processBuilder.environment().get("PATH"); - if (MAC_OS && path != null && !path.contains(USR_LOCAL_BIN) - && !command[0].startsWith(USR_LOCAL_BIN + "/")) { - String[] localCommand = command.clone(); - localCommand[0] = USR_LOCAL_BIN + "/" + localCommand[0]; - return startProcess(localCommand); + if (MAC_OS && path != null) { + for (String binDirectory : MAC_OS_BIN_DIRECTORIES) { + if (path.contains(binDirectory) || command[0].startsWith("/")) { + continue; + } + String[] localCommand = command.clone(); + localCommand[0] = binDirectory + "/" + command[0]; + ProcessBuilder localProcessBuilder = new ProcessBuilder(localCommand); + localProcessBuilder.directory(this.workingDirectory); + try { + return localProcessBuilder.start(); + } + catch (IOException suppressed) { + ex.addSuppressed(suppressed); + } + } } throw new ProcessStartException(command, ex); } diff --git a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java index d21f0ed37a41..f0fabb042731 100644 --- a/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java +++ b/core/spring-boot-docker-compose/src/main/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManager.java @@ -19,6 +19,7 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Set; import org.apache.commons.logging.Log; @@ -128,7 +129,13 @@ void start() { logger.info(skip.getLogMessage()); } else { - start.getCommand().applyTo(dockerCompose, start.getLogLevel(), start.getArguments()); + try { + start.getCommand().applyTo(dockerCompose, start.getLogLevel(), start.getArguments()); + } + catch (RuntimeException ex) { + logDockerComposeLogs(dockerCompose, start); + throw ex; + } runningServices = dockerCompose.getRunningServices(); if (wait == Wait.ONLY_IF_STARTED) { wait = Wait.ALWAYS; @@ -147,6 +154,28 @@ void start() { publishEvent(new DockerComposeServicesReadyEvent(this.applicationContext, relevantServices)); } + private void logDockerComposeLogs(DockerCompose dockerCompose, Start start) { + String command = start.getCommand().name().toLowerCase(Locale.ROOT); + String logs = retrieveLogsIfPossible(dockerCompose); + if (logs == null) { + start.getLogLevel() + .log(logger, "docker compose %s failed and its logs were unavailable".formatted(command)); + } + else { + start.getLogLevel() + .log(logger, "docker compose %s failed with the following logs:%n%n%s".formatted(command, logs)); + } + } + + private @Nullable String retrieveLogsIfPossible(DockerCompose dockerCompose) { + try { + return dockerCompose.logs(); + } + catch (Exception ex) { + return null; + } + } + protected DockerComposeFile getComposeFile() { DockerComposeFile composeFile = (CollectionUtils.isEmpty(this.properties.getFile())) ? DockerComposeFile.find(this.workingDirectory) : DockerComposeFile.of(this.properties.getFile()); diff --git a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java index 7aaeb7968122..f1d1126fa57c 100644 --- a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java +++ b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/core/DockerCliCommandTests.java @@ -43,7 +43,7 @@ void context() { DockerCliCommand command = new DockerCliCommand.Context(); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("context", "ls", "--format={{ json . }}"); - assertThat(command.deserialize("[]")).isInstanceOf(List.class); + assertThat(command.convert("[]")).isInstanceOf(List.class); } @Test @@ -52,7 +52,7 @@ void inspect() { assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("inspect", "--format={{ json . }}", "123", "345"); - assertThat(command.deserialize("[]")).isInstanceOf(List.class); + assertThat(command.convert("[]")).isInstanceOf(List.class); } @Test @@ -60,7 +60,7 @@ void composeConfig() { DockerCliCommand command = new DockerCliCommand.ComposeConfig(); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("config", "--format=json"); - assertThat(command.deserialize("{}")).isInstanceOf(DockerCliComposeConfigResponse.class); + assertThat(command.convert("{}")).isInstanceOf(DockerCliComposeConfigResponse.class); } @Test @@ -68,7 +68,7 @@ void composePs() { DockerCliCommand command = new DockerCliCommand.ComposePs(); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("ps", "--orphans=false", "--format=json"); - assertThat(command.deserialize("[]")).isInstanceOf(List.class); + assertThat(command.convert("[]")).isInstanceOf(List.class); } @Test @@ -76,7 +76,7 @@ void composePsWhenLessThanV224() { DockerCliCommand command = new DockerCliCommand.ComposePs(); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getCommand(ComposeVersion.of("2.23"))).containsExactly("ps", "--format=json"); - assertThat(command.deserialize("[]")).isInstanceOf(List.class); + assertThat(command.convert("[]")).isInstanceOf(List.class); } @Test @@ -86,7 +86,7 @@ void composeUp() { assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("up", "--no-color", "--detach", "--wait", "--renew-anon-volumes"); - assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE); + assertThat(command.convert("[]")).isSameAs(None.INSTANCE); } @Test @@ -95,7 +95,7 @@ void composeDown() { List.of("--remove-orphans")); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("down", "--timeout", "1", "--remove-orphans"); - assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE); + assertThat(command.convert("[]")).isSameAs(None.INSTANCE); } @Test @@ -104,7 +104,7 @@ void composeStart() { assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("start", "--dry-run"); - assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE); + assertThat(command.convert("[]")).isSameAs(None.INSTANCE); } @Test @@ -112,7 +112,7 @@ void composeStop() { DockerCliCommand command = new DockerCliCommand.ComposeStop(Duration.ofSeconds(1), List.of("--dry-run")); assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("stop", "--timeout", "1", "--dry-run"); - assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE); + assertThat(command.convert("[]")).isSameAs(None.INSTANCE); } @Test @@ -130,4 +130,18 @@ void composeVersionTests() { assertThat(versionWithPrefix.minor()).isEqualTo(31); } + @Test + void composeLogs() { + DockerCliCommand command = new DockerCliCommand.ComposeLogs(); + assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE); + assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("logs"); + assertThat(command.convert(""" + multi + line + logs""")).isEqualTo(""" + multi + line + logs"""); + } + } diff --git a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java index b6a978e1de78..027780383b97 100644 --- a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java +++ b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/lifecycle/DockerComposeLifecycleManagerTests.java @@ -50,10 +50,12 @@ import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -434,6 +436,46 @@ void shouldStartIfSkipModeIsNeverAndServicesAreAlreadyRunning() { then(this.dockerCompose).should().up(any(), any()); } + @Test + void whenStartFailsLogsAreRetrieved(CapturedOutput output) { + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + given(this.dockerCompose.logs()).willReturn(""" + multi + line + logs"""); + willThrow(RuntimeException.class).given(this.dockerCompose).up(any(), any()); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(this.lifecycleManager::start); + assertThat(output).contains("docker compose up failed with the following logs:").contains(""" + multi + line + logs"""); + } + + @Test + void whenStartUsesStartAndItFailsLogsAreRetrieved(CapturedOutput output) { + this.properties.getStart().setCommand(StartCommand.START); + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + given(this.dockerCompose.logs()).willReturn(""" + multi + line + logs"""); + willThrow(RuntimeException.class).given(this.dockerCompose).start(any(), any()); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(this.lifecycleManager::start); + assertThat(output).contains("docker compose start failed with the following logs:").contains(""" + multi + line + logs"""); + } + + @Test + void whenLogsAreUnavailableFailureIsHandled(CapturedOutput output) { + given(this.dockerCompose.hasDefinedServices()).willReturn(true); + given(this.dockerCompose.logs()).willThrow(RuntimeException.class); + willThrow(RuntimeException.class).given(this.dockerCompose).up(any(), any()); + assertThatExceptionOfType(RuntimeException.class).isThrownBy(this.lifecycleManager::start); + assertThat(output).contains("docker compose up failed and its logs were unavailable"); + } + private void setUpRunningServices() { setUpRunningServices(true); } diff --git a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java index a506e48f7958..7450bb47f828 100644 --- a/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java +++ b/core/spring-boot-docker-compose/src/test/java/org/springframework/boot/docker/compose/service/connection/ConnectionNamePredicateTests.java @@ -39,16 +39,17 @@ class ConnectionNamePredicateTests { @Test void official() { - assertThat(predicateOf("elasticsearch")).accepts(sourceOf("elasticsearch")); - assertThat(predicateOf("elasticsearch")).accepts(sourceOf("library/elasticsearch")); - assertThat(predicateOf("elasticsearch")).accepts(sourceOf("docker.io/library/elasticsearch")); - assertThat(predicateOf("elasticsearch")).accepts(sourceOf("docker.io/elasticsearch")); - assertThat(predicateOf("elasticsearch")).accepts(sourceOf("docker.io/elasticsearch:latest")); - assertThat(predicateOf("elasticsearch")).rejects(sourceOf("redis")); - assertThat(predicateOf("elasticsearch")).rejects(sourceOf("library/redis")); - assertThat(predicateOf("elasticsearch")).rejects(sourceOf("docker.io/library/redis")); - assertThat(predicateOf("elasticsearch")).rejects(sourceOf("docker.io/redis")); - assertThat(predicateOf("elasticsearch")).rejects(sourceOf("docker.io/redis")); + assertThat(predicateOf("elasticsearch", "elasticsearch/elasticsearch")).accepts(sourceOf("elasticsearch")) + .accepts(sourceOf("library/elasticsearch")) + .accepts(sourceOf("docker.io/library/elasticsearch")) + .accepts(sourceOf("docker.io/elasticsearch")) + .accepts(sourceOf("docker.io/elasticsearch:latest")) + .accepts(sourceOf("docker.elastic.co/elasticsearch/elasticsearch:latest")) + .accepts(sourceOf("docker.elastic.co/elasticsearch/elasticsearch")) + .rejects(sourceOf("redis")) + .rejects(sourceOf("library/redis")) + .rejects(sourceOf("docker.io/library/redis")) + .rejects(sourceOf("docker.io/redis")); assertThat(predicateOf("zipkin")).rejects(sourceOf("openzipkin/zipkin")); } diff --git a/core/spring-boot-docker-compose/src/testFixtures/java/org/springframework/boot/docker/compose/service/connection/test/DockerComposeTestExtension.java b/core/spring-boot-docker-compose/src/testFixtures/java/org/springframework/boot/docker/compose/service/connection/test/DockerComposeTestExtension.java index 6d97671042df..aa6a83f4ece7 100644 --- a/core/spring-boot-docker-compose/src/testFixtures/java/org/springframework/boot/docker/compose/service/connection/test/DockerComposeTestExtension.java +++ b/core/spring-boot-docker-compose/src/testFixtures/java/org/springframework/boot/docker/compose/service/connection/test/DockerComposeTestExtension.java @@ -60,18 +60,31 @@ class DockerComposeTestExtension implements BeforeTestExecutionCallback, AfterTe @Override public void beforeTestExecution(ExtensionContext context) throws Exception { + Path logFile = Files.createTempFile("docker-compose-test", ".log"); Store store = context.getStore(NAMESPACE); Path workspace = Files.createTempDirectory("DockerComposeTestExtension-"); store.put(STORE_KEY_WORKSPACE, workspace); try { Path composeFile = prepareComposeFile(workspace, context); copyAdditionalResources(workspace, context); - SpringApplication application = prepareApplication(composeFile); + SpringApplication application = prepareApplication(composeFile, logFile); store.put(STORE_KEY_APPLICATION_CONTEXT, application.run()); } catch (Exception ex) { cleanUp(context); - throw ex; + throw new DockerComposeApplicationRunException(readLogs(logFile)); + } + finally { + Files.delete(logFile); + } + } + + private String readLogs(Path logFile) { + try { + return Files.readString(logFile); + } + catch (IOException ex) { + return "Unavailable: " + ex.getMessage(); } } @@ -115,7 +128,7 @@ private void copyAdditionalResource(Path workspace, Resource resource) { } } - private SpringApplication prepareApplication(Path composeFile) { + private SpringApplication prepareApplication(Path composeFile, Path logFile) { SpringApplication application = new SpringApplication(Config.class); Map properties = new LinkedHashMap<>(); properties.put("spring.docker.compose.skip.in-tests", "false"); @@ -123,6 +136,7 @@ private SpringApplication prepareApplication(Path composeFile) { properties.put("spring.docker.compose.stop.command", "down"); properties.put("spring.docker.compose.start.arguments[0]", "--wait-timeout"); properties.put("spring.docker.compose.start.arguments[1]", "120"); + properties.put("logging.file.name", logFile.toString()); application.setDefaultProperties(properties); return application; } @@ -170,4 +184,12 @@ static class Config { } + static class DockerComposeApplicationRunException extends RuntimeException { + + DockerComposeApplicationRunException(String logs) { + super("Docker Compose failed. Application logs:%n%n%s".formatted(logs)); + } + + } + } diff --git a/core/spring-boot-test/build.gradle b/core/spring-boot-test/build.gradle index 5f8f7156e703..619e5c599074 100644 --- a/core/spring-boot-test/build.gradle +++ b/core/spring-boot-test/build.gradle @@ -54,10 +54,10 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.jetbrains.kotlin:kotlin-stdlib") testImplementation("org.slf4j:slf4j-api") + testImplementation("org.spockframework:spock-core") testImplementation("org.springframework:spring-webmvc") testImplementation("org.testng:testng") - testRuntimeOnly("org.apache.groovy:groovy") testRuntimeOnly("org.aspectj:aspectjrt") testRuntimeOnly("org.aspectj:aspectjweaver") testRuntimeOnly("org.junit.vintage:junit-vintage-engine") diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java index 70645fa416f6..04dbd7085b7a 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java @@ -244,6 +244,7 @@ static class ContextCustomizerKey { annotationFilters.add(AnnotationFilter.PLAIN); annotationFilters.add("kotlin.Metadata"::equals); annotationFilters.add(AnnotationFilter.packages("kotlin.annotation")); + annotationFilters.add(AnnotationFilter.packages("org.spockframework", "spock")); annotationFilters.add(AnnotationFilter.packages("org.junit")); ANNOTATION_FILTERS = Collections.unmodifiableSet(annotationFilters); } diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java index 9e3ba932f74a..74daba04d384 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssert.java @@ -36,6 +36,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.lang.CheckReturnValue; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; @@ -223,6 +224,7 @@ public ApplicationContextAssert doesNotHaveBean(String name) { * @return array assertions for the bean names * @throws AssertionError if the application context did not start */ + @CheckReturnValue public AbstractObjectArrayAssert getBeanNames(Class type) { if (this.startupFailure != null) { throwAssertionError(contextFailedToStartWhenExpecting(this.startupFailure, @@ -248,6 +250,7 @@ public AbstractObjectArrayAssert getBeanNames(Class type) { * @throws AssertionError if the application context contains multiple beans of the * given type */ + @CheckReturnValue public AbstractObjectAssert getBean(Class type) { return getBean(type, Scope.INCLUDE_ANCESTORS); } @@ -269,6 +272,7 @@ public AbstractObjectArrayAssert getBeanNames(Class type) { * @throws AssertionError if the application context contains multiple beans of the * given type */ + @CheckReturnValue public AbstractObjectAssert getBean(Class type, Scope scope) { Assert.notNull(scope, "'scope' must not be null"); if (this.startupFailure != null) { @@ -329,6 +333,7 @@ private boolean isPrimary(String name, Scope scope) { * is found * @throws AssertionError if the application context did not start */ + @CheckReturnValue public AbstractObjectAssert getBean(String name) { if (this.startupFailure != null) { throwAssertionError( @@ -356,6 +361,7 @@ private boolean isPrimary(String name, Scope scope) { * name but a different type */ @SuppressWarnings("unchecked") + @CheckReturnValue public AbstractObjectAssert getBean(String name, Class type) { if (this.startupFailure != null) { throwAssertionError(contextFailedToStartWhenExpecting(this.startupFailure, @@ -394,6 +400,7 @@ private boolean isPrimary(String name, Scope scope) { * no beans are found * @throws AssertionError if the application context did not start */ + @CheckReturnValue public MapAssert getBeans(Class type) { return getBeans(type, Scope.INCLUDE_ANCESTORS); } @@ -413,6 +420,7 @@ public MapAssert getBeans(Class type) { * no beans are found * @throws AssertionError if the application context did not start */ + @CheckReturnValue public MapAssert getBeans(Class type, Scope scope) { Assert.notNull(scope, "'scope' must not be null"); if (this.startupFailure != null) { @@ -433,6 +441,7 @@ public MapAssert getBeans(Class type, Scope scope) { * @return assertions on the cause of the failure * @throws AssertionError if the application context started without a failure */ + @CheckReturnValue public AbstractThrowableAssert getFailure() { hasFailed(); return assertThat(this.startupFailure); diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProvider.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProvider.java index 1db7a364f8fd..bae56ad3034f 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProvider.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProvider.java @@ -44,16 +44,18 @@ * Any {@link ApplicationContext} method called on a context that has failed to start will * throw an {@link IllegalStateException}. * - * @param the application context type + * @param the self class reference + * @param the source application context type * @author Phillip Webb + * @author Stefano Cordio * @since 2.0.0 * @see AssertableApplicationContext * @see AssertableWebApplicationContext * @see AssertableReactiveWebApplicationContext * @see ApplicationContextAssert */ -public interface ApplicationContextAssertProvider - extends ApplicationContext, AssertProvider>, Closeable { +public interface ApplicationContextAssertProvider, C extends ApplicationContext> + extends ApplicationContext, AssertProvider>, Closeable { /** * Return an assert for AssertJ. @@ -63,7 +65,7 @@ public interface ApplicationContextAssertProvider */ @Deprecated(since = "2.0.0", forRemoval = false) @Override - ApplicationContextAssert assertThat(); + ApplicationContextAssert assertThat(); /** * Return the original source {@link ApplicationContext}. @@ -104,7 +106,7 @@ public interface ApplicationContextAssertProvider * {@link ApplicationContext} or throw an exception if the context fails to start. * @return a {@link ApplicationContextAssertProvider} instance */ - static , C extends ApplicationContext> T get(Class type, + static , C extends ApplicationContext> T get(Class type, Class contextType, Supplier contextSupplier) { return get(type, contextType, contextSupplier, new Class[0]); } @@ -125,7 +127,7 @@ static , C extends ApplicationCont * @since 3.4.0 */ @SuppressWarnings("unchecked") - static , C extends ApplicationContext> T get(Class type, + static , C extends ApplicationContext> T get(Class type, Class contextType, Supplier contextSupplier, Class... additionalContextInterfaces) { Assert.notNull(type, "'type' must not be null"); diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableApplicationContext.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableApplicationContext.java index db4cbc32f97a..4a79387d005d 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableApplicationContext.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableApplicationContext.java @@ -35,7 +35,8 @@ * @see ApplicationContext */ public interface AssertableApplicationContext - extends ApplicationContextAssertProvider, ConfigurableApplicationContext { + extends ApplicationContextAssertProvider, + ConfigurableApplicationContext { /** * Factory method to create a new {@link AssertableApplicationContext} instance. diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableReactiveWebApplicationContext.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableReactiveWebApplicationContext.java index 78a349d0ff86..a6fe90e87cd1 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableReactiveWebApplicationContext.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableReactiveWebApplicationContext.java @@ -33,8 +33,8 @@ * @see ReactiveWebApplicationContext * @see ReactiveWebApplicationContext */ -public interface AssertableReactiveWebApplicationContext - extends ApplicationContextAssertProvider, +public interface AssertableReactiveWebApplicationContext extends + ApplicationContextAssertProvider, ConfigurableReactiveWebApplicationContext { /** diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableWebApplicationContext.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableWebApplicationContext.java index ba61598ec6b7..1a6094d256d6 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableWebApplicationContext.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/assertj/AssertableWebApplicationContext.java @@ -35,7 +35,8 @@ * @see WebApplicationContext */ public interface AssertableWebApplicationContext - extends ApplicationContextAssertProvider, ConfigurableWebApplicationContext { + extends ApplicationContextAssertProvider, + ConfigurableWebApplicationContext { /** * Factory method to create a new {@link AssertableWebApplicationContext} instance. diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java index 7afc3a65ece9..9b9477078c98 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunner.java @@ -110,7 +110,7 @@ * @see ReactiveWebApplicationContextRunner * @see ApplicationContextAssert */ -public abstract class AbstractApplicationContextRunner, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { +public abstract class AbstractApplicationContextRunner, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { private final RunnerConfiguration runnerConfiguration; diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java index 00cca319d69c..74a7cab3020f 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/JsonContentAssert.java @@ -39,6 +39,7 @@ import org.skyscreamer.jsonassert.comparator.JSONComparator; import org.springframework.core.io.Resource; +import org.springframework.lang.CheckReturnValue; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.util.function.ThrowingFunction; @@ -918,6 +919,7 @@ public JsonContentAssert doesNotHaveEmptyJsonPathValue(CharSequence expression, * @return a new assertion object whose object under test is the extracted item * @throws AssertionError if the path is not valid */ + @CheckReturnValue public AbstractObjectAssert extractingJsonPathValue(CharSequence expression, Object... args) { return assertThat(new JsonPathValue(expression, args).getValue(false)); } @@ -930,6 +932,7 @@ public AbstractObjectAssert extractingJsonPathValue(CharSequence expr * @return a new assertion object whose object under test is the extracted item * @throws AssertionError if the path is not valid or does not result in a string */ + @CheckReturnValue public AbstractCharSequenceAssert extractingJsonPathStringValue(CharSequence expression, Object... args) { return assertThat(extractingJsonPathValue(expression, args, String.class, "a string")); @@ -943,6 +946,7 @@ public AbstractCharSequenceAssert extractingJsonPathStringValue(CharS * @return a new assertion object whose object under test is the extracted item * @throws AssertionError if the path is not valid or does not result in a number */ + @CheckReturnValue public AbstractObjectAssert extractingJsonPathNumberValue(CharSequence expression, Object... args) { return assertThat(extractingJsonPathValue(expression, args, Number.class, "a number")); } @@ -955,6 +959,7 @@ public AbstractObjectAssert extractingJsonPathNumberValue(CharSequenc * @return a new assertion object whose object under test is the extracted item * @throws AssertionError if the path is not valid or does not result in a boolean */ + @CheckReturnValue public AbstractBooleanAssert extractingJsonPathBooleanValue(CharSequence expression, Object... args) { return assertThat(extractingJsonPathValue(expression, args, Boolean.class, "a boolean")); } @@ -969,6 +974,7 @@ public AbstractBooleanAssert extractingJsonPathBooleanValue(CharSequence expr * @throws AssertionError if the path is not valid or does not result in an array */ @SuppressWarnings("unchecked") + @CheckReturnValue public ListAssert extractingJsonPathArrayValue(CharSequence expression, Object... args) { return assertThat(extractingJsonPathValue(expression, args, List.class, "an array")); } @@ -984,6 +990,7 @@ public ListAssert extractingJsonPathArrayValue(CharSequence expression, O * @throws AssertionError if the path is not valid or does not result in a map */ @SuppressWarnings("unchecked") + @CheckReturnValue public MapAssert extractingJsonPathMapValue(CharSequence expression, Object... args) { return assertThat(extractingJsonPathValue(expression, args, Map.class, "a map")); } diff --git a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java index d58612f85959..d406ee93c21c 100644 --- a/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java +++ b/core/spring-boot-test/src/main/java/org/springframework/boot/test/json/ObjectContentAssert.java @@ -22,6 +22,8 @@ import org.assertj.core.api.Assert; import org.assertj.core.api.InstanceOfAssertFactories; +import org.springframework.lang.CheckReturnValue; + /** * AssertJ {@link Assert} for {@link ObjectContent}. * @@ -41,6 +43,7 @@ protected ObjectContentAssert(A actual) { * allow chaining of array-specific assertions from this call. * @return an array assertion object */ + @CheckReturnValue public AbstractObjectArrayAssert asArray() { return asInstanceOf(InstanceOfAssertFactories.ARRAY); } @@ -50,6 +53,7 @@ public AbstractObjectArrayAssert asArray() { * chaining of map-specific assertions from this call. * @return a map assertion object */ + @CheckReturnValue public AbstractMapAssert asMap() { return asInstanceOf(InstanceOfAssertFactories.MAP); } diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java index c6fd97346d45..ee9369d4469a 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerTests.java @@ -25,6 +25,9 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.spockframework.runtime.model.SpecMetadata; +import spock.lang.Issue; +import spock.lang.Stepwise; import org.springframework.boot.context.annotation.DeterminableImports; import org.springframework.context.annotation.Configuration; @@ -61,6 +64,18 @@ void customizersForTestClassesWithDifferentKotlinMetadataAreEqual() { .isEqualTo(new ImportsContextCustomizer(SecondKotlinAnnotatedTestClass.class)); } + @Test + void customizersForTestClassesWithDifferentSpockFrameworkAnnotationsAreEqual() { + assertThat(new ImportsContextCustomizer(FirstSpockFrameworkAnnotatedTestClass.class)) + .isEqualTo(new ImportsContextCustomizer(SecondSpockFrameworkAnnotatedTestClass.class)); + } + + @Test + void customizersForTestClassesWithDifferentSpockLangAnnotationsAreEqual() { + assertThat(new ImportsContextCustomizer(FirstSpockLangAnnotatedTestClass.class)) + .isEqualTo(new ImportsContextCustomizer(SecondSpockLangAnnotatedTestClass.class)); + } + @Test void customizersForTestClassesWithDifferentJUnitAnnotationsAreEqual() { assertThat(new ImportsContextCustomizer(FirstJUnitAnnotatedTestClass.class)) @@ -127,6 +142,30 @@ static class SecondKotlinAnnotatedTestClass { } + @SpecMetadata(filename = "foo", line = 10) + @Import(TestImportSelector.class) + static class FirstSpockFrameworkAnnotatedTestClass { + + } + + @SpecMetadata(filename = "bar", line = 10) + @Import(TestImportSelector.class) + static class SecondSpockFrameworkAnnotatedTestClass { + + } + + @Stepwise + @Import(TestImportSelector.class) + static class FirstSpockLangAnnotatedTestClass { + + } + + @Issue("1234") + @Import(TestImportSelector.class) + static class SecondSpockLangAnnotatedTestClass { + + } + @Nested @Import(TestImportSelector.class) static class FirstJUnitAnnotatedTestClass { diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java index e2269cbf6d38..f54784483976 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/assertj/ApplicationContextAssertProviderTests.java @@ -100,7 +100,7 @@ void getWhenSupplierIsNullShouldThrowException() { @Test void getWhenContextStartsShouldReturnProxyThatCallsRealMethods() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat((Object) context).isNotNull(); context.getBean("foo"); then(this.mockContext).should().getBean("foo"); @@ -108,7 +108,7 @@ void getWhenContextStartsShouldReturnProxyThatCallsRealMethods() { @Test void getWhenContextFailsShouldReturnProxyThatThrowsExceptions() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); assertThat((Object) context).isNotNull(); assertThatIllegalStateException().isThrownBy(() -> context.getBean("foo")) .withCause(this.startupFailure) @@ -117,13 +117,13 @@ void getWhenContextFailsShouldReturnProxyThatThrowsExceptions() { @Test void getSourceContextWhenContextStartsShouldReturnSourceContext() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat(context.getSourceApplicationContext()).isSameAs(this.mockContext); } @Test void getSourceContextWhenContextFailsShouldThrowException() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); assertThatIllegalStateException().isThrownBy(context::getSourceApplicationContext) .withCause(this.startupFailure) .withMessageContaining("failed to start"); @@ -131,13 +131,13 @@ void getSourceContextWhenContextFailsShouldThrowException() { @Test void getSourceContextOfTypeWhenContextStartsShouldReturnSourceContext() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat(context.getSourceApplicationContext(ApplicationContext.class)).isSameAs(this.mockContext); } @Test void getSourceContextOfTypeWhenContextFailsToStartShouldThrowException() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); assertThatIllegalStateException() .isThrownBy(() -> context.getSourceApplicationContext(ApplicationContext.class)) .withCause(this.startupFailure) @@ -146,59 +146,60 @@ void getSourceContextOfTypeWhenContextFailsToStartShouldThrowException() { @Test void getStartupFailureWhenContextStartsShouldReturnNull() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat(context.getStartupFailure()).isNull(); } @Test void getStartupFailureWhenContextFailsToStartShouldReturnException() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); assertThat(context.getStartupFailure()).isEqualTo(this.startupFailure); } @Test void assertThatWhenContextStartsShouldReturnAssertions() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); - ApplicationContextAssert contextAssert = assertThat(context); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssert contextAssert = assertThat(context); assertThat(contextAssert.getApplicationContext()).isSameAs(context); assertThat(contextAssert.getStartupFailure()).isNull(); } @Test void assertThatWhenContextFailsShouldReturnAssertions() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); - ApplicationContextAssert contextAssert = assertThat(context); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssert contextAssert = assertThat(context); assertThat(contextAssert.getApplicationContext()).isSameAs(context); assertThat(contextAssert.getStartupFailure()).isSameAs(this.startupFailure); } @Test void toStringWhenContextStartsShouldReturnSimpleString() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); assertThat(context.toString()).startsWith("Started application [ConfigurableApplicationContext.MockitoMock") .endsWith("id = [null], applicationName = [null], beanDefinitionCount = 0]"); } @Test void toStringWhenContextFailsToStartShouldReturnSimpleString() { - ApplicationContextAssertProvider context = get(this.startupFailureSupplier); + ApplicationContextAssertProvider context = get(this.startupFailureSupplier); assertThat(context).hasToString("Unstarted application context " + "org.springframework.context.ApplicationContext[startupFailure=java.lang.RuntimeException]"); } @Test void closeShouldCloseContext() { - ApplicationContextAssertProvider context = get(this.mockContextSupplier); + ApplicationContextAssertProvider context = get(this.mockContextSupplier); context.close(); then(this.mockContext).should().close(); } - private ApplicationContextAssertProvider get(Supplier contextSupplier) { + private ApplicationContextAssertProvider get(Supplier contextSupplier) { return ApplicationContextAssertProvider.get(TestAssertProviderApplicationContext.class, ApplicationContext.class, contextSupplier); } - interface TestAssertProviderApplicationContext extends ApplicationContextAssertProvider { + interface TestAssertProviderApplicationContext + extends ApplicationContextAssertProvider { } diff --git a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java index 3ab607b4b387..adee92a0230d 100644 --- a/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java +++ b/core/spring-boot-test/src/test/java/org/springframework/boot/test/context/runner/AbstractApplicationContextRunnerTests.java @@ -62,8 +62,9 @@ * @param the assertable context type * @author Stephane Nicoll * @author Phillip Webb + * @author Stefano Cordio */ -abstract class AbstractApplicationContextRunnerTests, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { +abstract class AbstractApplicationContextRunnerTests, C extends ConfigurableApplicationContext, A extends ApplicationContextAssertProvider> { @Test void runWithInitializerShouldInitialize() { @@ -278,6 +279,12 @@ void runWithConfigurationsAndUserBeanShouldRegisterUserBeanLast() { }); } + @Test + void runShouldWorkWithSatisfiesAssertion() { + get().run((context) -> assertThat(context).satisfies((ctx) -> assertThat(ctx).hasNotFailed(), + (ctx) -> assertThat(ctx).doesNotHaveBean("foo"))); + } + @Test void changesMadeByInitializersShouldBeVisibleToRegisteredClasses() { get().withInitializer((context) -> context.getEnvironment().setActiveProfiles("test")) diff --git a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/DockerEnvironmentNotFoundFailureAnalyzer.java b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/DockerEnvironmentNotFoundFailureAnalyzer.java new file mode 100644 index 000000000000..0b096d704841 --- /dev/null +++ b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/lifecycle/DockerEnvironmentNotFoundFailureAnalyzer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-present 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.testcontainers.lifecycle; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.util.StringUtils; + +/** + * A {@code FailureAnalyzer} that performs analysis of failures caused by Testcontainers + * not finding a valid Docker environment. + * + * @author Dmytro Nosan + * @author Stephane Nicoll + */ +class DockerEnvironmentNotFoundFailureAnalyzer extends AbstractFailureAnalyzer { + + private static final String EXPECTED_MESSAGE = "Could not find a valid Docker environment"; + + private static final String DESCRIPTION = "Could not find a valid Docker environment for Testcontainers."; + + private static final String ACTION = """ + - Ensure a Docker-compatible container engine is installed and running. + - If running Testcontainers in CI, ensure the runner has access to the daemon, typically by using a mounted socket or a Docker-in-Docker setup. + - Review the Testcontainers documentation for troubleshooting and advanced configuration options. + """; + + @Override + protected @Nullable FailureAnalysis analyze(Throwable rootFailure, IllegalStateException cause) { + if (StringUtils.hasText(cause.getMessage()) && cause.getMessage().contains(EXPECTED_MESSAGE)) { + return new FailureAnalysis(DESCRIPTION, ACTION, cause); + } + return null; + } + +} diff --git a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java index f08067721e23..76a23ab82b95 100644 --- a/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java +++ b/core/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/ContainerConnectionSource.java @@ -167,7 +167,12 @@ Supplier getContainerSupplier() { return this.containerSupplier; } - Set> getConnectionDetailsTypes() { + /** + * Returns the requested connection details types. + * @return the requested connection details types. + * @since 4.1.0 + */ + public Set> getConnectionDetailsTypes() { return this.connectionDetailsTypes; } diff --git a/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories b/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories index 965d32bf8b61..c816883c85fb 100644 --- a/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories +++ b/core/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories @@ -6,3 +6,6 @@ org.springframework.boot.testcontainers.lifecycle.TestcontainersLifecycleApplica org.springframework.test.context.ContextCustomizerFactory=\ org.springframework.boot.testcontainers.service.connection.ServiceConnectionContextCustomizerFactory +# Failure Analyzers +org.springframework.boot.diagnostics.FailureAnalyzer=\ +org.springframework.boot.testcontainers.lifecycle.DockerEnvironmentNotFoundFailureAnalyzer diff --git a/core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/DockerEnvironmentNotFoundFailureAnalyzerTests.java b/core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/DockerEnvironmentNotFoundFailureAnalyzerTests.java new file mode 100644 index 000000000000..2a777e738159 --- /dev/null +++ b/core/spring-boot-testcontainers/src/test/java/org/springframework/boot/testcontainers/lifecycle/DockerEnvironmentNotFoundFailureAnalyzerTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2012-present 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.testcontainers.lifecycle; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.diagnostics.FailureAnalysis; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link DockerEnvironmentNotFoundFailureAnalyzer}. + * + * @author Dmytro Nosan + */ +class DockerEnvironmentNotFoundFailureAnalyzerTests { + + private final DockerEnvironmentNotFoundFailureAnalyzer analyzer = new DockerEnvironmentNotFoundFailureAnalyzer(); + + @Test + void shouldReturnFailureAnalysisWhenMessageMatches() { + IllegalStateException cause = new IllegalStateException( + "Could not find a valid Docker environment. Please see logs and check configuration"); + FailureAnalysis analysis = this.analyzer + .analyze(new RuntimeException("Root", new RuntimeException("Intermediate", cause))); + assertThat(analysis).isNotNull(); + assertThat(analysis.getDescription()) + .isEqualTo("Could not find a valid Docker environment for Testcontainers."); + assertThat(analysis.getAction()) + .contains("Ensure a Docker-compatible container engine is installed and running"); + assertThat(analysis.getCause()).isSameAs(cause); + } + + @Test + void shouldReturnNullWhenMessageDoesNotMatch() { + FailureAnalysis analysis = this.analyzer.analyze(new IllegalStateException("Some message")); + assertThat(analysis).isNull(); + } + + @Test + void shouldReturnNullWhenMessageIsNull() { + FailureAnalysis analysis = this.analyzer.analyze(new IllegalStateException()); + assertThat(analysis).isNull(); + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java b/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java index 661edccbe3ef..c5d5af28bae5 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java @@ -383,6 +383,13 @@ private void prepareContext(DefaultBootstrapContext bootstrapContext, Configurab context.setEnvironment(environment); postProcessApplicationContext(context); addAotGeneratedInitializerIfNecessary(this.initializers); + ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); + if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) { + autowireCapableBeanFactory.setAllowCircularReferences(this.properties.isAllowCircularReferences()); + if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) { + listableBeanFactory.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding()); + } + } applyInitializers(context); listeners.contextPrepared(context); bootstrapContext.close(context); @@ -391,17 +398,10 @@ private void prepareContext(DefaultBootstrapContext bootstrapContext, Configurab logStartupProfileInfo(context); } // Add boot specific singleton beans - ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton("springApplicationArguments", applicationArguments); if (printedBanner != null) { beanFactory.registerSingleton("springBootBanner", printedBanner); } - if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) { - autowireCapableBeanFactory.setAllowCircularReferences(this.properties.isAllowCircularReferences()); - if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) { - listableBeanFactory.setAllowBeanDefinitionOverriding(this.properties.isAllowBeanDefinitionOverriding()); - } - } if (this.properties.isLazyInitialization()) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java index 22f7afffd107..c0d9f272f0b0 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiOutput.java @@ -18,6 +18,7 @@ import java.io.Console; import java.lang.reflect.Method; +import java.util.List; import java.util.Locale; import org.jspecify.annotations.Nullable; @@ -31,6 +32,7 @@ * * @author Phillip Webb * @author Yong-Hyun Kim + * @author Philemon Hilscher * @since 1.0.0 */ public abstract class AnsiOutput { @@ -43,7 +45,9 @@ public abstract class AnsiOutput { private static @Nullable Boolean ansiCapable; - private static final String OPERATING_SYSTEM_NAME = System.getProperty("os.name").toLowerCase(Locale.ENGLISH); + private static final String OPERATING_SYSTEM_NAME = System.getProperty("os.name"); + + private static final List WINDOWS_ANSI_COMPATIBLE = List.of("Windows 11", "Windows Server 2025"); private static final String ENCODE_START = "\033["; @@ -171,7 +175,10 @@ private static boolean detectIfAnsiCapable() { } } } - return !(OPERATING_SYSTEM_NAME.contains("win")); + if (OPERATING_SYSTEM_NAME.toLowerCase(Locale.ENGLISH).contains("win")) { + return WINDOWS_ANSI_COMPATIBLE.contains(OPERATING_SYSTEM_NAME); + } + return true; } catch (Throwable ex) { return false; diff --git a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java index c30a88c2bfb5..0d2a734f37b9 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiStyle.java @@ -32,7 +32,9 @@ public enum AnsiStyle implements AnsiElement { ITALIC("3"), - UNDERLINE("4"); + UNDERLINE("4"), + + REVERSE("7"); private final String code; diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileExtensionHint.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileExtensionHint.java deleted file mode 100644 index 891975bed645..000000000000 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileExtensionHint.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2012-present 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.context.config; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.jspecify.annotations.Nullable; - -/** - * User-provided hint for an otherwise missing file extension. - * - * @author Phillip Webb - */ -final class FileExtensionHint { - - private static final Pattern PATTERN = Pattern.compile("^(.*)\\[(\\.\\w+)](?!\\[)$"); - - private static final FileExtensionHint NONE = new FileExtensionHint(null); - - private final @Nullable Matcher matcher; - - private FileExtensionHint(@Nullable Matcher matcher) { - this.matcher = matcher; - } - - /** - * Return {@code true} if the hint is present. - * @return if the hint is present - */ - boolean isPresent() { - return this.matcher != null; - } - - /** - * Return the extension from the hint or return the parameter if the hint is not - * {@link #isPresent() present}. - * @param extension the fallback extension - * @return the extension either from the hint or fallback - */ - String orElse(String extension) { - return (this.matcher != null) ? toString() : extension; - } - - @Override - public String toString() { - return (this.matcher != null) ? this.matcher.group(2) : ""; - } - - /** - * Return the {@link FileExtensionHint} from the given value. - * @param value the source value - * @return the {@link FileExtensionHint} (never {@code null}) - */ - static FileExtensionHint from(String value) { - Matcher matcher = PATTERN.matcher(value); - return (matcher.matches()) ? new FileExtensionHint(matcher) : NONE; - } - - /** - * Remove any hint from the given value. - * @param value the source value - * @return the value without any hint - */ - static String removeFrom(String value) { - Matcher matcher = PATTERN.matcher(value); - return (matcher.matches()) ? matcher.group(1) : value; - } - -} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileHint.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileHint.java new file mode 100644 index 000000000000..9e385012caea --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/FileHint.java @@ -0,0 +1,188 @@ +/* + * Copyright 2012-present 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.context.config; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jspecify.annotations.Nullable; + +/** + * User-provided hint for file attributes, like extension or encoding. + * + * @author Phillip Webb + * @author Moritz Halbritter + */ +final class FileHint { + + private static final FileHint NONE = new FileHint(Collections.emptyMap()); + + private final Map attributes; + + private FileHint(Map attributes) { + this.attributes = attributes; + } + + /** + * Return {@code true} if the hint is present. + * @return if the hint is present + */ + boolean isPresent() { + return getExtension() != null || getEncoding() != null; + } + + /** + * Returns the extension. + * @return the extension + */ + @Nullable String getExtension() { + String extension = this.attributes.get("extension"); + if (extension == null) { + return null; + } + if (!extension.startsWith(".")) { + return null; + } + return extension; + } + + /** + * Return the extension from the hint or return the {@code fallback} if the extension + * is not present. + * @param fallback the fallback extension + * @return the extension either from the hint or fallback + */ + String getExtensionOrElse(String fallback) { + String extension = getExtension(); + return (extension != null) ? extension : fallback; + } + + /** + * Returns the encoding. + * @return the encoding + */ + @Nullable String getEncoding() { + return this.attributes.get("encoding"); + } + + /** + * Returns the encoding as a {@link Charset}. + * @return the encoding as a {@link Charset} + */ + @Nullable Charset getEncodingAsCharset() { + String encoding = getEncoding(); + return (encoding != null) ? Charset.forName(encoding) : null; + } + + @Override + public String toString() { + return "FileHint{attributes=" + this.attributes + '}'; + } + + /** + * Return the {@link FileHint} from the given value. + * @param value the source value + * @return the {@link FileHint} + */ + static FileHint from(String value) { + if (!hasBrackets(value)) { + return NONE; + } + List betweenBrackets = findBetweenBrackets(value); + Map attributes = new HashMap<>(); + for (String entry : betweenBrackets) { + String cleaned = entry.trim(); + if (cleaned.isEmpty()) { + continue; + } + int equals = cleaned.indexOf('='); + if (equals == -1) { + attributes.put("extension", cleaned); + } + else { + attributes.put(cleaned.substring(0, equals).trim(), cleaned.substring(equals + 1).trim()); + } + } + return new FileHint(Collections.unmodifiableMap(attributes)); + } + + private static List findBetweenBrackets(String value) { + List collected = new ArrayList<>(); + StringBuilder buffer = new StringBuilder(); + int brackets = 0; + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '[') { + if (brackets > 0) { + buffer.append(c); + } + brackets++; + } + else if (c == ']') { + brackets--; + if (brackets > 0) { + buffer.append(c); + } + else if (brackets == 0 && !buffer.isEmpty()) { + collected.add(buffer.toString()); + buffer.setLength(0); + } + } + else if (brackets > 0) { + buffer.append(c); + } + } + return collected; + } + + /** + * Remove any hint from the given value. + * @param value the source value + * @return the value without any hint + */ + static String removeFrom(String value) { + if (!hasBrackets(value)) { + return value; + } + StringBuilder result = new StringBuilder(); + int brackets = 0; + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '[') { + brackets++; + continue; + } + if (c == ']') { + brackets--; + continue; + } + if (brackets <= 0) { + result.append(c); + } + } + return result.toString(); + } + + private static boolean hasBrackets(String value) { + return value.indexOf('[') != -1 && value.indexOf(']') != -1; + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLoader.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLoader.java index 4245f0d37214..468881933934 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLoader.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLoader.java @@ -51,7 +51,8 @@ public ConfigData load(ConfigDataLoaderContext context, StandardConfigDataResour Origin.from(reference.getConfigDataLocation())); String name = String.format("Config resource '%s' via location '%s'", resource, reference.getConfigDataLocation()); - List> propertySources = reference.getPropertySourceLoader().load(name, originTrackedResource); + List> propertySources = reference.getPropertySourceLoader() + .load(name, originTrackedResource, reference.getEncoding()); PropertySourceOptions options = (resource.getProfile() != null) ? PROFILE_SPECIFIC : NON_PROFILE_SPECIFIC; return new ConfigData(propertySources, options); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java index fe49fc84c7bf..ae3a5a09190c 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java @@ -203,7 +203,7 @@ private Deque getReferencesForConfigName(String nam for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) { for (String extension : propertySourceLoader.getFileExtensions()) { StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, directory, - directory + name, profile, extension, propertySourceLoader); + directory + name, profile, extension, propertySourceLoader, null); if (!references.contains(reference)) { references.addFirst(reference); } @@ -214,16 +214,19 @@ private Deque getReferencesForConfigName(String nam private Set getReferencesForFile(ConfigDataLocation configDataLocation, String file, @Nullable String profile) { - FileExtensionHint fileExtensionHint = FileExtensionHint.from(file); - if (fileExtensionHint.isPresent()) { - file = FileExtensionHint.removeFrom(file) + fileExtensionHint; + FileHint fileHint = FileHint.from(file); + file = FileHint.removeFrom(file); + boolean hasFileHintExtension = fileHint.getExtension() != null; + if (hasFileHintExtension) { + file = file + fileHint.getExtension(); } for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) { String fileExtension = getLoadableFileExtension(propertySourceLoader, file); if (fileExtension != null) { String root = file.substring(0, file.length() - fileExtension.length() - 1); StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, null, root, - profile, (!fileExtensionHint.isPresent()) ? fileExtension : null, propertySourceLoader); + profile, (!hasFileHintExtension) ? fileExtension : null, propertySourceLoader, + fileHint.getEncodingAsCharset()); return Collections.singleton(reference); } } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java index f2b1000871c9..9df98ecfc915 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java @@ -16,6 +16,8 @@ package org.springframework.boot.context.config; +import java.nio.charset.Charset; + import org.jspecify.annotations.Nullable; import org.springframework.boot.env.PropertySourceLoader; @@ -26,6 +28,7 @@ * be resolved to one or more {@link StandardConfigDataResource resources}. * * @author Phillip Webb + * @author Moritz Halbritter */ class StandardConfigDataReference { @@ -39,6 +42,8 @@ class StandardConfigDataReference { private final PropertySourceLoader propertySourceLoader; + private final @Nullable Charset encoding; + /** * Create a new {@link StandardConfigDataReference} instance. * @param configDataLocation the original location passed to the resolver @@ -49,15 +54,18 @@ class StandardConfigDataReference { * @param extension the file extension for the resource * @param propertySourceLoader the property source loader that should be used for this * reference + * @param encoding the encoding of the resource */ StandardConfigDataReference(ConfigDataLocation configDataLocation, @Nullable String directory, String root, - @Nullable String profile, @Nullable String extension, PropertySourceLoader propertySourceLoader) { + @Nullable String profile, @Nullable String extension, PropertySourceLoader propertySourceLoader, + @Nullable Charset encoding) { this.configDataLocation = configDataLocation; String profileSuffix = (StringUtils.hasText(profile)) ? "-" + profile : ""; this.resourceLocation = root + profileSuffix + ((extension != null) ? "." + extension : ""); this.directory = directory; this.profile = profile; this.propertySourceLoader = propertySourceLoader; + this.encoding = encoding; } ConfigDataLocation getConfigDataLocation() { @@ -80,6 +88,10 @@ boolean isMandatoryDirectory() { return this.profile; } + @Nullable Charset getEncoding() { + return this.encoding; + } + boolean isSkippable() { return this.configDataLocation.isOptional() || this.directory != null || this.profile != null; } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLocationResolver.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLocationResolver.java index 18849fb7312a..6f95be76b72e 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLocationResolver.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataLocationResolver.java @@ -63,11 +63,12 @@ public List resolve(ConfigDataLocationResol ConfigDataLocation location) throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException { String value = location.getNonPrefixedValue(PREFIX); - FileExtensionHint fileExtensionHint = FileExtensionHint.from(value); - String variableName = FileExtensionHint.removeFrom(value); - PropertySourceLoader loader = getLoader(fileExtensionHint.orElse(DEFAULT_EXTENSION)); + FileHint fileHint = FileHint.from(value); + String variableName = FileHint.removeFrom(value); + PropertySourceLoader loader = getLoader(fileHint.getExtensionOrElse(DEFAULT_EXTENSION)); if (hasEnvVariable(variableName)) { - return List.of(new SystemEnvironmentConfigDataResource(variableName, loader, this.environment)); + return List.of(new SystemEnvironmentConfigDataResource(variableName, loader, this.environment, + fileHint.getEncodingAsCharset())); } if (location.isOptional()) { return Collections.emptyList(); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResource.java b/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResource.java index 53a62c2e9798..a681b0d73dae 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResource.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/config/SystemEnvironmentConfigDataResource.java @@ -17,6 +17,7 @@ package org.springframework.boot.context.config; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; @@ -43,11 +44,14 @@ class SystemEnvironmentConfigDataResource extends ConfigDataResource { private final Function environment; + private final @Nullable Charset encoding; + SystemEnvironmentConfigDataResource(String variableName, PropertySourceLoader loader, - Function environment) { + Function environment, @Nullable Charset encoding) { this.variableName = variableName; this.loader = loader; this.environment = environment; + this.encoding = encoding; } String getVariableName() { @@ -60,7 +64,8 @@ PropertySourceLoader getLoader() { @Nullable List> load() throws IOException { String content = this.environment.apply(this.variableName); - return (content != null) ? this.loader.load(StringUtils.capitalize(toString()), asResource(content)) : null; + return (content != null) + ? this.loader.load(StringUtils.capitalize(toString()), asResource(content), this.encoding) : null; } private ByteArrayResource asResource(String content) { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java index 29ac4e3fb232..a6fd1c7c8263 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindConverter.java @@ -98,14 +98,12 @@ private boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor t @SuppressWarnings("unchecked") @Nullable T convert(@Nullable Object source, ResolvableType targetType, Annotation... targetAnnotations) { - if (source == null) { - return null; - } - return (T) convert(source, TypeDescriptor.forObject(source), - new ResolvableTypeDescriptor(targetType, targetAnnotations)); + TypeDescriptor sourceType = (source != null) ? TypeDescriptor.forObject(source) : null; + return (T) convert(source, sourceType, new ResolvableTypeDescriptor(targetType, targetAnnotations)); } - private @Nullable Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + private @Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, + TypeDescriptor targetType) { ConversionException failure = null; for (ConversionService delegate : this.delegates) { try { @@ -119,6 +117,9 @@ private boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor t } } } + if (source == null) { + return null; + } throw (failure != null) ? failure : new ConverterNotFoundException(sourceType, targetType); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java index ac0298e1c9af..e42afcb502fc 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java @@ -49,6 +49,7 @@ import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.ObjectUtils; /** * A container object which Binds objects from one or more @@ -435,14 +436,16 @@ public T bindOrCreate(ConfigurationPropertyName name, Bindable target, @N } catch (ConverterNotFoundException ex) { // We might still be able to bind it using the recursive binders - Object instance = bindDataObject(name, target, handler, context, allowRecursiveBinding); + boolean fallbackToDefaultValue = ObjectUtils.isEmpty(property.getValue()); + Object instance = bindDataObject(name, target, handler, context, allowRecursiveBinding, + fallbackToDefaultValue); if (instance != null) { return instance; } throw ex; } } - return bindDataObject(name, target, handler, context, allowRecursiveBinding); + return bindDataObject(name, target, handler, context, allowRecursiveBinding, false); } private @Nullable AggregateBinder getAggregateBinder(Bindable target, Context context) { @@ -493,7 +496,7 @@ public T bindOrCreate(ConfigurationPropertyName name, Bindable target, @N } private @Nullable Object bindDataObject(ConfigurationPropertyName name, Bindable target, BindHandler handler, - Context context, boolean allowRecursiveBinding) { + Context context, boolean allowRecursiveBinding, boolean fallbackToDefaultValue) { if (isUnbindableBean(name, target, context)) { return null; } @@ -505,7 +508,8 @@ public T bindOrCreate(ConfigurationPropertyName name, Bindable target, @N DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName), propertyTarget, handler, context, false, false); Supplier<@Nullable Object> supplier = () -> fromDataObjectBinders(bindMethod, - (dataObjectBinder) -> dataObjectBinder.bind(name, target, context, propertyBinder)); + (dataObjectBinder) -> dataObjectBinder.bind(name, target, context, propertyBinder, + fallbackToDefaultValue)); return context.withDataObject(type, supplier); } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java index dfd41d8616dc..6fb0d26f6bfd 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectBinder.java @@ -40,10 +40,12 @@ interface DataObjectBinder { * @param target the bindable to bind * @param context the bind context * @param propertyBinder property binder + * @param fallbackToDefaultValue if an attempt should be made to return a new default + * value when no values are bound * @return a bound instance or {@code null} */ @Nullable T bind(ConfigurationPropertyName name, Bindable target, Context context, - DataObjectPropertyBinder propertyBinder); + DataObjectPropertyBinder propertyBinder, boolean fallbackToDefaultValue); /** * Return a newly created instance or {@code null} if the {@link DataObjectBinder} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java index 92cdbced79d8..33532663571e 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java @@ -61,7 +61,7 @@ class JavaBeanBinder implements DataObjectBinder { @Override public @Nullable T bind(ConfigurationPropertyName name, Bindable target, Context context, - DataObjectPropertyBinder propertyBinder) { + DataObjectPropertyBinder propertyBinder, boolean fallbackToDefaultValue) { boolean hasKnownBindableProperties = target.getValue() != null && hasKnownBindableProperties(name, context); Bean bean = Bean.get(target, context, hasKnownBindableProperties); if (bean == null) { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java index f1e57fa32eee..db40be47d8c0 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java @@ -39,6 +39,7 @@ * * @author Phillip Webb * @author Madhura Bhave + * @author Ahmed El Amraouiyine */ class MapBinder extends AggregateBinder> { @@ -64,6 +65,9 @@ protected boolean isAllowRecursiveBinding(@Nullable ConfigurationPropertySource if (property != null) { getContext().setConfigurationProperty(property); Object result = getContext().getPlaceholdersResolver().resolvePlaceholders(property.getValue()); + if (result instanceof CharSequence charSequence && charSequence.isEmpty()) { + return createMap(target); + } return getContext().getConverter().convert(result, target); } } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java index 8f2337ace58f..9bdab5489961 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ValueObjectBinder.java @@ -61,6 +61,7 @@ * @author Stephane Nicoll * @author Phillip Webb * @author Scott Frederick + * @author Ondřej Světlík */ class ValueObjectBinder implements DataObjectBinder { @@ -74,26 +75,31 @@ class ValueObjectBinder implements DataObjectBinder { @Override public @Nullable T bind(ConfigurationPropertyName name, Bindable target, Binder.Context context, - DataObjectPropertyBinder propertyBinder) { + DataObjectPropertyBinder propertyBinder, boolean fallbackToDefaultValue) { ValueObject valueObject = ValueObject.get(target, context, this.constructorProvider, Discoverer.LENIENT); - if (valueObject == null) { - return null; + if (valueObject != null) { + Class targetType = target.getType().resolve(); + Assert.state(targetType != null, "'targetType' must not be null"); + context.pushConstructorBoundTypes(targetType); + List parameters = valueObject.getConstructorParameters(); + List<@Nullable Object> args = new ArrayList<>(parameters.size()); + boolean bound = false; + for (ConstructorParameter parameter : parameters) { + Object arg = parameter.bind(propertyBinder); + bound = bound || arg != null; + arg = (arg != null) ? arg : getDefaultValue(context, parameter); + args.add(arg); + } + context.clearConfigurationProperty(); + context.popConstructorBoundTypes(); + if (bound) { + return valueObject.instantiate(args); + } } - Class targetType = target.getType().resolve(); - Assert.state(targetType != null, "'targetType' must not be null"); - context.pushConstructorBoundTypes(targetType); - List parameters = valueObject.getConstructorParameters(); - List<@Nullable Object> args = new ArrayList<>(parameters.size()); - boolean bound = false; - for (ConstructorParameter parameter : parameters) { - Object arg = parameter.bind(propertyBinder); - bound = bound || arg != null; - arg = (arg != null) ? arg : getDefaultValue(context, parameter); - args.add(arg); - } - context.clearConfigurationProperty(); - context.popConstructorBoundTypes(); - return bound ? valueObject.instantiate(args) : null; + if (fallbackToDefaultValue) { + return getNewDefaultValueInstanceIfPossible(context, target.getType()); + } + return null; } @Override @@ -132,7 +138,7 @@ public void onUnableToCreateInstance(Bindable target, Context context, Ru return convertDefaultValue(context.getConverter(), defaultValue, type, annotations); } } - return null; + return context.getConverter().convert(null, type); } private @Nullable T convertDefaultValue(BindConverter converter, String[] defaultValue, ResolvableType type, diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/InvalidConfigurationPropertyValueException.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/InvalidConfigurationPropertyValueException.java index 3f7619a63883..d94bdd3d013d 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/InvalidConfigurationPropertyValueException.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/InvalidConfigurationPropertyValueException.java @@ -48,7 +48,18 @@ public InvalidConfigurationPropertyValueException(String name, @Nullable Object this(name, value, reason, null); } - InvalidConfigurationPropertyValueException(String name, @Nullable Object value, @Nullable String reason, + /** + * Creates a new instance for the specified property {@code name} and {@code value}, + * including a {@code reason} why the value is invalid. + * @param name the name of the property in canonical format + * @param value the value of the property, can be {@code null} + * @param reason a human-readable text that describes why the value is invalid. Starts + * with an upper-case character and ends with a dot. Several sentences and lines are + * allowed. + * @param cause the cause of the exception or {@code null} + * @since 4.1.0 + */ + public InvalidConfigurationPropertyValueException(String name, @Nullable Object value, @Nullable String reason, @Nullable Throwable cause) { super("Property " + name + " with value '" + value + "' is invalid: " + reason, cause); Assert.notNull(name, "'name' must not be null"); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzedException.java b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzedException.java new file mode 100644 index 000000000000..d4110f7e8043 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzedException.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-present 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.diagnostics; + +import org.jspecify.annotations.Nullable; + +/** + * {@link RuntimeException} that includes a {@link FailureAnalysis}. + * + * @author Phillip Webb + * @since 4.1.0 + */ +public class FailureAnalyzedException extends RuntimeException { + + private final @Nullable String action; + + /** + * Create a new {@link FailureAnalyzedException} instance. + * @param description the {@link FailureAnalysis} description + * @param action the {@link FailureAnalysis} action + */ + public FailureAnalyzedException(String description, @Nullable String action) { + super(description); + this.action = action; + } + + /** + * Create a new {@link FailureAnalyzedException} instance. + * @param description the {@link FailureAnalysis} description + * @param action the {@link FailureAnalysis} action + * @param cause the cause of the exception + */ + public FailureAnalyzedException(String description, @Nullable String action, Throwable cause) { + super(description, cause); + this.action = action; + } + + /** + * Return the {@link FailureAnalysis} to use for this exception. + * @return the failure analysis + */ + public FailureAnalysis analysis() { + return new FailureAnalysis(getMessage(), this.action, this); + } + + static @Nullable FailureAnalysis analyze(Throwable failure) { + while (failure != null) { + if (failure instanceof FailureAnalyzedException failureAnalyzedException) { + return failureAnalyzedException.analysis(); + } + failure = failure.getCause(); + } + return null; + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java index 6c967fa3a48e..c56f2129b7ba 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/diagnostics/FailureAnalyzers.java @@ -16,6 +16,8 @@ package org.springframework.boot.diagnostics; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.apache.commons.logging.Log; @@ -64,8 +66,10 @@ public FailureAnalyzers(@Nullable ConfigurableApplicationContext context) { private static List loadFailureAnalyzers(@Nullable ConfigurableApplicationContext context, SpringFactoriesLoader springFactoriesLoader) { - return springFactoriesLoader.load(FailureAnalyzer.class, getArgumentResolver(context), - FailureHandler.logging(logger)); + List analyzers = new ArrayList<>(springFactoriesLoader.load(FailureAnalyzer.class, + getArgumentResolver(context), FailureHandler.logging(logger))); + analyzers.add(FailureAnalyzedException::analyze); + return Collections.unmodifiableList(analyzers); } private static @Nullable ArgumentResolver getArgumentResolver(@Nullable ConfigurableApplicationContext context) { diff --git a/core/spring-boot/src/main/java/org/springframework/boot/env/EnvironmentPostProcessor.java b/core/spring-boot/src/main/java/org/springframework/boot/env/EnvironmentPostProcessor.java deleted file mode 100644 index 08ccbaf1d7a7..000000000000 --- a/core/spring-boot/src/main/java/org/springframework/boot/env/EnvironmentPostProcessor.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2012-present 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.env; - -import org.springframework.boot.bootstrap.BootstrapContext; -import org.springframework.boot.bootstrap.BootstrapRegistry; -import org.springframework.boot.bootstrap.ConfigurableBootstrapContext; -import org.springframework.boot.logging.DeferredLogFactory; -import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.core.env.Environment; - -/** - * Allows for customization of the application's {@link Environment} prior to the - * application context being refreshed. - *

- * EnvironmentPostProcessor implementations have to be registered in - * {@code META-INF/spring.factories}, using the fully qualified name of this class as the - * key. Implementations may implement the {@link org.springframework.core.Ordered Ordered} - * interface or use an {@link org.springframework.core.annotation.Order @Order} annotation - * if they wish to be invoked in specific order. - *

- * Since Spring Boot 2.4, {@code EnvironmentPostProcessor} implementations may optionally - * take the following constructor parameters: - *

- * - * @author Andy Wilkinson - * @author Stephane Nicoll - * @since 1.3.0 - * @deprecated since 4.0.0 for removal in 4.2.0 in favor of - * {@link org.springframework.boot.EnvironmentPostProcessor} - */ -@FunctionalInterface -@Deprecated(since = "4.0.0", forRemoval = true) -public interface EnvironmentPostProcessor { - - /** - * Post-process the given {@code environment}. - * @param environment the environment to post-process - * @param application the application to which the environment belongs - */ - void postProcessEnvironment(ConfigurableEnvironment environment, - org.springframework.boot.SpringApplication application); - -} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java b/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java index 8e0ff1b0892e..eff6e306a312 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedPropertiesLoader.java @@ -20,6 +20,7 @@ import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -27,6 +28,8 @@ import java.util.Map; import java.util.function.BooleanSupplier; +import org.jspecify.annotations.Nullable; + import org.springframework.boot.origin.Origin; import org.springframework.boot.origin.OriginTrackedValue; import org.springframework.boot.origin.TextResourceOrigin; @@ -43,6 +46,7 @@ * @author Phillip Webb * @author Thiago Hirata * @author Guirong Hu + * @author Moritz Halbritter */ class OriginTrackedPropertiesLoader { @@ -59,25 +63,27 @@ class OriginTrackedPropertiesLoader { /** * Load {@code .properties} data and return a list of documents. + * @param encoding the resource encoding. Uses ISO-8859-1 if {@code null} * @return the loaded properties * @throws IOException on read error */ - List load() throws IOException { - return load(true); + List load(@Nullable Charset encoding) throws IOException { + return load(encoding, true); } /** * Load {@code .properties} data and return a map of {@code String} -> * {@link OriginTrackedValue}. + * @param encoding the resource encoding. Uses ISO-8859-1 if {@code null} * @param expandLists if list {@code name[]=a,b,c} shortcuts should be expanded * @return the loaded properties * @throws IOException on read error */ - List load(boolean expandLists) throws IOException { + List load(@Nullable Charset encoding, boolean expandLists) throws IOException { List documents = new ArrayList<>(); Document document = new Document(); StringBuilder buffer = new StringBuilder(); - try (CharacterReader reader = new CharacterReader(this.resource)) { + try (CharacterReader reader = new CharacterReader(this.resource, encoding)) { while (reader.read()) { if (reader.isCommentPrefixCharacter()) { char commentPrefixCharacter = reader.getCharacter(); @@ -200,9 +206,9 @@ private static class CharacterReader implements Closeable { private int lastLineCommentPrefixCharacter; - CharacterReader(Resource resource) throws IOException { - this.reader = new LineNumberReader( - new InputStreamReader(resource.getInputStream(), StandardCharsets.ISO_8859_1)); + CharacterReader(Resource resource, @Nullable Charset encoding) throws IOException { + this.reader = new LineNumberReader(new InputStreamReader(resource.getInputStream(), + (encoding != null) ? encoding : StandardCharsets.ISO_8859_1)); } @Override diff --git a/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java b/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java index 258181225336..82f1811acf28 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/env/OriginTrackedYamlLoader.java @@ -82,7 +82,7 @@ private Yaml createYaml(LoaderOptions loaderOptions) { List> load() { List> result = new ArrayList<>(); - process((properties, map) -> result.add(getFlattenedMap(map))); + process((properties, map) -> result.add(getFlattenedMap(map, true, ""))); return result; } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java b/core/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java index 91c807e68cb4..3f11dc8334be 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java @@ -17,14 +17,18 @@ package org.springframework.boot.env; import java.io.IOException; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.boot.env.OriginTrackedPropertiesLoader.Document; import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; +import org.springframework.core.io.support.EncodedResource; import org.springframework.core.io.support.PropertiesLoaderUtils; /** @@ -46,7 +50,12 @@ public String[] getFileExtensions() { @Override public List> load(String name, Resource resource) throws IOException { - List> properties = loadProperties(resource); + return load(name, resource, null); + } + + @Override + public List> load(String name, Resource resource, @Nullable Charset encoding) throws IOException { + List> properties = loadProperties(resource, encoding); if (properties.isEmpty()) { return Collections.emptyList(); } @@ -60,14 +69,19 @@ public List> load(String name, Resource resource) throws IOExc } @SuppressWarnings({ "unchecked", "rawtypes" }) - private List> loadProperties(Resource resource) throws IOException { + private List> loadProperties(Resource resource, @Nullable Charset encoding) throws IOException { String filename = resource.getFilename(); List> result = new ArrayList<>(); if (filename != null && filename.endsWith(XML_FILE_EXTENSION)) { - result.add((Map) PropertiesLoaderUtils.loadProperties(resource)); + if (encoding == null) { + result.add((Map) PropertiesLoaderUtils.loadProperties(resource)); + } + else { + result.add((Map) PropertiesLoaderUtils.loadProperties(new EncodedResource(resource, encoding))); + } } else { - List documents = new OriginTrackedPropertiesLoader(resource).load(); + List documents = new OriginTrackedPropertiesLoader(resource).load(encoding); documents.forEach((document) -> result.add(document.asMap())); } return result; diff --git a/core/spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java b/core/spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java index ce8276e3ae48..76570cabc13b 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java @@ -17,8 +17,11 @@ package org.springframework.boot.env; import java.io.IOException; +import java.nio.charset.Charset; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.SpringFactoriesLoader; @@ -51,4 +54,21 @@ public interface PropertySourceLoader { */ List> load(String name, Resource resource) throws IOException; + /** + * Load the resource into one or more property sources. Implementations may either + * return a list containing a single source, or in the case of a multi-document format + * such as yaml a source for each document in the resource. + * @param name the root name of the property source. If multiple documents are loaded + * an additional suffix should be added to the name for each source loaded. + * @param resource the resource to load + * @param encoding encoding of the resource + * @return a list property sources + * @throws IOException if the source cannot be loaded + * @since 4.1.0 + */ + default List> load(String name, Resource resource, @Nullable Charset encoding) + throws IOException { + return load(name, resource); + } + } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java b/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java index e66876322363..1a8151987466 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/info/ProcessInfo.java @@ -16,13 +16,19 @@ package org.springframework.boot.info; +import java.lang.ProcessHandle.Info; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryUsage; import java.lang.management.PlatformManagedObject; import java.lang.reflect.Method; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; import java.util.List; +import java.util.Locale; import org.jspecify.annotations.Nullable; @@ -50,11 +56,23 @@ public class ProcessInfo { private final @Nullable String owner; + private final @Nullable Instant startTime; + + private final ZoneId timezone; + + private final Locale locale; + + private final String workingDirectory; + public ProcessInfo() { ProcessHandle process = ProcessHandle.current(); this.pid = process.pid(); this.parentPid = process.parent().map(ProcessHandle::pid).orElse(-1L); this.owner = process.info().user().orElse(null); + this.startTime = process.info().startInstant().orElse(null); + this.timezone = ZoneId.systemDefault(); + this.locale = Locale.getDefault(); + this.workingDirectory = Path.of(".").toAbsolutePath().normalize().toString(); } /** @@ -133,6 +151,68 @@ public long getParentPid() { return this.owner; } + /** + * Uptime of the process. Can be useful to see how long the process has been running + * and to check how long ago the last deployment or restart happened. + * @return duration since the process started, if available, otherwise {@code null} + * @since 4.1.0 + */ + public @Nullable Duration getUptime() { + return (this.startTime != null) ? Duration.between(this.startTime, Instant.now()) : null; + } + + /** + * Time at which the process started. Can be useful to see when the process was + * started and to check when the last deployment or restart happened. + * @return the time when the process started, if available, otherwise {@code null} + * @since 4.1.0 + * @see Info#startInstant() + */ + public @Nullable Instant getStartTime() { + return this.startTime; + } + + /** + * Current time of the process. Can be useful to check if there is any clock-skew + * issue and if the current time that the process knows is accurate enough. + * @return the current time of the process + * @since 4.1.0 + * @see Instant#now + */ + public Instant getCurrentTime() { + return Instant.now(); + } + + /** + * Timezone of the process. Can help to detect time and timezone related issues. + * @return the timezone of the process + * @since 4.1.0 + * @see ZoneId#systemDefault() + */ + public ZoneId getTimezone() { + return this.timezone; + } + + /** + * Locale of the process. Can help to detect issues connected to language and country + * settings. + * @return the locale of the process + * @since 4.1.0 + * @see Locale#getDefault() + */ + public Locale getLocale() { + return this.locale; + } + + /** + * Working directory of the process. Can help to locate files that the process uses. + * @return the absolute path of the working directory of the process + * @since 4.1.0 + */ + public String getWorkingDirectory() { + return this.workingDirectory; + } + /** * Virtual threads information. * diff --git a/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java b/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java index 46da37d4e64b..b3be703613d4 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/info/SslInfo.java @@ -104,9 +104,12 @@ public final class BundleInfo { private final List certificateChains; + private final List trustStoreCertificateChains; + private BundleInfo(String name, SslBundle sslBundle) { this.name = name; this.certificateChains = extractCertificateChains(sslBundle.getStores().getKeyStore()); + this.trustStoreCertificateChains = extractCertificateChains(sslBundle.getStores().getTrustStore()); } private List extractCertificateChains(@Nullable KeyStore keyStore) { @@ -132,6 +135,10 @@ public List getCertificateChains() { return this.certificateChains; } + public List getTrustStoreCertificateChains() { + return this.trustStoreCertificateChains; + } + } /** @@ -150,15 +157,34 @@ public final class CertificateChainInfo { private List extractCertificates(KeyStore keyStore, String alias) { try { - Certificate[] certificates = keyStore.getCertificateChain(alias); - return (!ObjectUtils.isEmpty(certificates)) - ? Arrays.stream(certificates).map(CertificateInfo::new).toList() : Collections.emptyList(); + List certificates = readCertificateChain(keyStore, alias); + if (certificates != null) { + return certificates; + } + List certificate = readCertificate(keyStore, alias); + if (certificate != null) { + return certificate; + } + return Collections.emptyList(); } catch (KeyStoreException ex) { return Collections.emptyList(); } } + private @Nullable List readCertificate(KeyStore keyStore, String alias) + throws KeyStoreException { + Certificate certificate = keyStore.getCertificate(alias); + return (certificate != null) ? List.of(new CertificateInfo(certificate)) : null; + } + + private @Nullable List readCertificateChain(KeyStore keyStore, String alias) + throws KeyStoreException { + Certificate[] certificates = keyStore.getCertificateChain(alias); + return ObjectUtils.isEmpty(certificates) ? null + : Arrays.stream(certificates).map(CertificateInfo::new).toList(); + } + public String getAlias() { return this.alias; } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java new file mode 100644 index 000000000000..e0274f6be61c --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/AppendableByteArray.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012-present 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.json; + +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; + +import org.jspecify.annotations.Nullable; + +import org.springframework.util.Assert; +import org.springframework.util.function.ThrowingConsumer; + +/** + * {@link Appendable} implementation that can be used to return a byte array. Designed to + * reduce memory pressure for {@link WritableJson#toByteArray(Charset)} by using a single + * cached buffer scoped to the thread. + * + * @author Phillip Webb + * @author Stephane Nicoll + */ +class AppendableByteArray implements Appendable { + + private static final ThreadLocal<@Nullable SoftReference> cache = new ThreadLocal<>(); + + private static final int DEFAULT_INITIAL_SIZE = 8192; + + private static final int DEFAULT_EXPANSION_SIZE = 8192; + + private static final byte[] NO_BYTES = {}; + + private final Charset charset; + + private final CharsetEncoder encoder; + + private final int expansionSize; + + private ByteBuffer out; + + AppendableByteArray(Charset charset) { + this(charset, DEFAULT_INITIAL_SIZE, DEFAULT_EXPANSION_SIZE); + } + + AppendableByteArray(Charset charset, int initialSize, int expansionSize) { + this.charset = charset; + this.encoder = charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPLACE) + .onUnmappableCharacter(CodingErrorAction.REPLACE); + this.out = ByteBuffer.allocate(initialSize); + this.expansionSize = expansionSize; + } + + @Override + public AppendableByteArray append(@Nullable CharSequence charSequence, int start, int end) throws IOException { + return append(((charSequence != null) ? charSequence : "null").subSequence(start, end)); + } + + @Override + public AppendableByteArray append(CharSequence charSequence) throws IOException { + return append(String.valueOf(charSequence).toCharArray()); + } + + @Override + public AppendableByteArray append(char ch) throws IOException { + return append(new char[] { ch }); + } + + private AppendableByteArray append(char[] chars) throws IOException { + return (chars.length != 0) ? append(CharBuffer.wrap(chars)) : this; + } + + private AppendableByteArray append(CharBuffer in) throws IOException { + CoderResult result = this.encoder.encode(in, this.out, false); + if (result.isUnderflow()) { + return this; + } + if (result.isOverflow()) { + ByteBuffer out = this.out; + this.out = ByteBuffer.allocate(out.capacity() + this.expansionSize); + out.flip(); + this.out.put(out); + return append(in); + } + result.throwException(); + return this; + } + + byte[] toByteArray() { + this.out.flip(); + int limit = this.out.limit(); + int position = this.out.position(); + int size = limit - position; + if (size <= 0) { + return NO_BYTES; + } + byte[] result = new byte[size]; + System.arraycopy(this.out.array(), this.out.arrayOffset() + position, result, 0, size); + return result; + } + + private void reset() { + this.out.clear(); + this.encoder.reset(); + } + + static byte[] toByteArray(Charset charset, ThrowingConsumer appendable) throws IOException { + Assert.notNull(charset, "'charset' must not be null"); + Assert.notNull(appendable, "'appendable' must not be null"); + AppendableByteArray appendableByteArray = get(charset); + try { + appendable.acceptWithException(appendableByteArray); + return appendableByteArray.toByteArray(); + } + catch (IOException | RuntimeException ex) { + throw ex; + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + finally { + appendableByteArray.reset(); + } + } + + private static AppendableByteArray get(Charset charset) { + Assert.notNull(charset, "'charset' must not be null"); + SoftReference cached = cache.get(); + AppendableByteArray result = (cached != null) ? cached.get() : null; + if (result == null || !result.charset.equals(charset)) { + result = new AppendableByteArray(charset); + cache.set(new SoftReference<>(result)); + } + return result; + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java b/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java index 16932268ee46..457b4848309d 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/json/WritableJson.java @@ -16,7 +16,6 @@ package org.springframework.boot.json; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; @@ -76,9 +75,8 @@ default byte[] toByteArray() { */ default byte[] toByteArray(Charset charset) { Assert.notNull(charset, "'charset' must not be null"); - try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { - toWriter(new OutputStreamWriter(out, charset)); - return out.toByteArray(); + try { + return AppendableByteArray.toByteArray(charset, this::to); } catch (IOException ex) { throw new UncheckedIOException(ex); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java index af4a4d64921d..ba2bc58b35bc 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/ColorConverter.java @@ -16,6 +16,7 @@ package org.springframework.boot.logging.log4j2; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -35,6 +36,7 @@ import org.apache.logging.log4j.core.pattern.PatternParser; import org.jspecify.annotations.Nullable; +import org.springframework.boot.ansi.AnsiBackground; import org.springframework.boot.ansi.AnsiColor; import org.springframework.boot.ansi.AnsiElement; import org.springframework.boot.ansi.AnsiOutput; @@ -42,8 +44,11 @@ /** * Log4j2 {@link LogEventPatternConverter} to color output using the {@link AnsiOutput} - * class. A single option 'styling' can be provided to the converter, or if not specified - * color styling will be picked based on the logging level. + * class. One or more styling options can be provided to the converter, or if not + * specified color styling will be picked based on the logging level. Supported options + * include foreground colors (e.g. {@code red}, {@code bright_blue}), background colors + * (e.g. {@code bg_red}, {@code bg_bright_green}), and text styles (e.g. {@code bold}, + * {@code underline}, {@code reverse}). * * @author Vladimir Tsanev * @since 1.3.0 @@ -59,7 +64,11 @@ public final class ColorConverter extends LogEventPatternConverter { Arrays.stream(AnsiColor.values()) .filter((color) -> color != AnsiColor.DEFAULT) .forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color)); - ansiElements.put("faint", AnsiStyle.FAINT); + Arrays.stream(AnsiStyle.values()) + .forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style)); + Arrays.stream(AnsiBackground.values()) + .filter((bg) -> bg != AnsiBackground.DEFAULT) + .forEach((bg) -> ansiElements.put("bg_" + bg.name().toLowerCase(Locale.ROOT), bg)); ELEMENTS = Collections.unmodifiableMap(ansiElements); } @@ -75,12 +84,12 @@ public final class ColorConverter extends LogEventPatternConverter { private final List formatters; - private final @Nullable AnsiElement styling; + private final List stylings; - private ColorConverter(List formatters, @Nullable AnsiElement styling) { + private ColorConverter(List formatters, List stylings) { super("style", "style"); this.formatters = formatters; - this.styling = styling; + this.stylings = stylings; } @Override @@ -100,13 +109,15 @@ public void format(LogEvent event, StringBuilder toAppendTo) { formatter.format(event, buf); } if (!buf.isEmpty()) { - AnsiElement element = this.styling; - if (element == null) { + if (this.stylings.isEmpty()) { // Assume highlighting - element = LEVELS.get(event.getLevel().intLevel()); + AnsiElement element = LEVELS.get(event.getLevel().intLevel()); element = (element != null) ? element : AnsiColor.GREEN; + appendAnsiString(toAppendTo, buf.toString(), element); + } + else { + appendAnsiString(toAppendTo, buf.toString(), this.stylings.toArray(new AnsiElement[0])); } - appendAnsiString(toAppendTo, buf.toString(), element); } } @@ -114,6 +125,13 @@ private void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement e toAppendTo.append(AnsiOutput.toString(element, in)); } + protected void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement... elements) { + Object[] ansiParams = new Object[elements.length + 1]; + System.arraycopy(elements, 0, ansiParams, 0, elements.length); + ansiParams[elements.length] = in; + toAppendTo.append(AnsiOutput.toString(ansiParams)); + } + /** * Creates a new instance of the class. Required by Log4J2. * @param config the configuration @@ -131,8 +149,17 @@ private void appendAnsiString(StringBuilder toAppendTo, String in, AnsiElement e } PatternParser parser = PatternLayout.createPatternParser(config); List formatters = parser.parse(options[0]); - AnsiElement element = (options.length != 1) ? ELEMENTS.get(options[1]) : null; - return new ColorConverter(formatters, element); + List stylings = new ArrayList<>(); + if (options.length >= 2 && options[1] != null) { + String[] optionParts = options[1].split(","); + for (String optionPart : optionParts) { + AnsiElement element = ELEMENTS.get(optionPart.trim().toLowerCase(Locale.ROOT)); + if (element != null) { + stylings.add(element); + } + } + } + return new ColorConverter(formatters, stylings); } } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index 8ca48215727b..e2285bfa6c43 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -58,8 +58,10 @@ import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemFactory; +import org.springframework.boot.logging.LoggingSystemProperties; import org.springframework.core.Conventions; import org.springframework.core.annotation.Order; +import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; @@ -134,6 +136,11 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { this.loggerContext = (LoggerContext) spiLoggerContext; } + @Override + public LoggingSystemProperties getSystemProperties(ConfigurableEnvironment environment) { + return new Log4j2LoggingSystemProperties(environment, getDefaultValueResolver(environment), null); + } + @Override protected String[] getStandardConfigLocations() { // With Log4J2 we use the ConfigurationFactory diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java new file mode 100644 index 000000000000..4be71e3f622c --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4j2LoggingSystemProperties.java @@ -0,0 +1,101 @@ +/* + * Copyright 2012-present 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.logging.log4j2; + +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.logging.LogFile; +import org.springframework.boot.logging.LoggingSystemProperties; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.env.Environment; +import org.springframework.core.env.PropertyResolver; +import org.springframework.util.unit.DataSize; + +/** + * {@link LoggingSystemProperties} for Log4j2. + * + * @author HoJoo Moon + * @since 4.1.0 + * @see RollingPolicySystemProperty + */ +public class Log4j2LoggingSystemProperties extends LoggingSystemProperties { + + public Log4j2LoggingSystemProperties(Environment environment) { + super(environment); + } + + /** + * Create a new {@link Log4j2LoggingSystemProperties} instance. + * @param environment the source environment + * @param defaultValueResolver function used to resolve default values or {@code null} + * @param setter setter used to apply the property or {@code null} for system + * properties + */ + public Log4j2LoggingSystemProperties(Environment environment, + Function<@Nullable String, @Nullable String> defaultValueResolver, + @Nullable BiConsumer setter) { + super(environment, defaultValueResolver, setter); + } + + @Override + protected void apply(@Nullable LogFile logFile, PropertyResolver resolver) { + super.apply(logFile, resolver); + applyRollingPolicyProperties(resolver); + } + + private void applyRollingPolicyProperties(PropertyResolver resolver) { + applyRollingPolicy(RollingPolicySystemProperty.FILE_NAME_PATTERN, resolver); + applyRollingPolicy(RollingPolicySystemProperty.MAX_FILE_SIZE, resolver, DataSize.class); + applyRollingPolicy(RollingPolicySystemProperty.MAX_HISTORY, resolver); + applyRollingPolicy(RollingPolicySystemProperty.STRATEGY, resolver); + applyRollingPolicy(RollingPolicySystemProperty.TIME_INTERVAL, resolver, Integer.class); + applyRollingPolicy(RollingPolicySystemProperty.TIME_MODULATE, resolver, Boolean.class); + applyRollingPolicy(RollingPolicySystemProperty.CRON, resolver); + } + + private void applyRollingPolicy(RollingPolicySystemProperty property, PropertyResolver resolver) { + applyRollingPolicy(property, resolver, String.class); + } + + private void applyRollingPolicy(RollingPolicySystemProperty property, PropertyResolver resolver, + Class type) { + T value = getProperty(resolver, property.getApplicationPropertyName(), type); + if (value != null) { + String stringValue = String.valueOf((value instanceof DataSize dataSize) ? dataSize.toBytes() : value); + setSystemProperty(property.getEnvironmentVariableName(), stringValue); + } + } + + @SuppressWarnings("unchecked") + private @Nullable T getProperty(PropertyResolver resolver, String key, Class type) { + try { + return resolver.getProperty(key, type); + } + catch (ConversionFailedException | ConverterNotFoundException ex) { + if (type != DataSize.class) { + throw ex; + } + String value = resolver.getProperty(key); + return (value != null) ? (T) DataSize.parse(value) : null; + } + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/RollingPolicyStrategy.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/RollingPolicyStrategy.java new file mode 100644 index 000000000000..1655fe903a27 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/RollingPolicyStrategy.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-present 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.logging.log4j2; + +/** + * Available rolling policy strategies. + * + * @author Stephane Nicoll + * @since 4.1.0 + */ +public enum RollingPolicyStrategy { + + /** + * Roll a file over based on its size. + */ + SIZE, + + /** + * Roll a file over based on time. + */ + TIME, + + /** + * Roll a file over based on its size and time. + */ + SIZE_AND_TIME, + + /** + * Roll a file over based on a cron schedule. + */ + CRON + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/RollingPolicySystemProperty.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/RollingPolicySystemProperty.java new file mode 100644 index 000000000000..f5524cf4d3ca --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/RollingPolicySystemProperty.java @@ -0,0 +1,87 @@ +/* + * Copyright 2012-present 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.logging.log4j2; + +/** + * Log4j2 rolling policy system properties that can later be used by log configuration + * files. + * + * @author HoJoo Moon + * @author Stephane Nicoll + * @since 4.1.0 + * @see Log4j2LoggingSystemProperties + */ +public enum RollingPolicySystemProperty { + + /** + * Logging system property for the rolled-over log file name pattern. + */ + FILE_NAME_PATTERN("file-name-pattern"), + + /** + * Logging system property for the file log max size. + */ + MAX_FILE_SIZE("max-file-size"), + + /** + * Logging system property for the file log max history. + */ + MAX_HISTORY("max-history"), + + /** + * Logging system property for the {@linkplain RollingPolicyStrategy rolling policy + * strategy}. + */ + STRATEGY("strategy"), + + /** + * Logging system property for the rolling policy time interval. + */ + TIME_INTERVAL("time-interval"), + + /** + * Logging system property for the rolling policy time modulate flag. + */ + TIME_MODULATE("time-modulate"), + + /** + * Logging system property for the cron based schedule. + */ + CRON("cron"); + + private final String environmentVariableName; + + private final String applicationPropertyName; + + RollingPolicySystemProperty(String applicationPropertyName) { + this.environmentVariableName = "LOG4J2_ROLLINGPOLICY_" + name(); + this.applicationPropertyName = "logging.log4j2.rollingpolicy." + applicationPropertyName; + } + + /** + * Return the name of environment variable that can be used to access this property. + * @return the environment variable name + */ + public String getEnvironmentVariableName() { + return this.environmentVariableName; + } + + String getApplicationPropertyName() { + return this.applicationPropertyName; + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java new file mode 100644 index 000000000000..fb4c31e5e656 --- /dev/null +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringBootTriggeringPolicy.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-present 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.logging.log4j2; + +import java.util.Objects; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.rolling.CompositeTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.CronTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.RollingFileManager; +import org.apache.logging.log4j.core.appender.rolling.SizeBasedTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.TimeBasedTriggeringPolicy; +import org.apache.logging.log4j.core.appender.rolling.TriggeringPolicy; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; +import org.apache.logging.log4j.core.util.Builder; +import org.jspecify.annotations.Nullable; + +import org.springframework.boot.convert.ApplicationConversionService; +import org.springframework.core.convert.ConversionException; +import org.springframework.util.Assert; + +/** + * Factory for creating a standard Log4j2 {@link TriggeringPolicy} based on configuration + * attributes. The supported strategies are {@code size}, {@code time}, + * {@code size-and-time}, and {@code cron}. + * + * @author HoJoo Moon + * @author Stephane Nicoll + * @since 4.1.0 + */ +@Plugin(name = "SpringBootTriggeringPolicy", category = Node.CATEGORY, elementType = "TriggeringPolicy", + deferChildren = true, printObject = true) +public abstract class SpringBootTriggeringPolicy implements TriggeringPolicy { + + private SpringBootTriggeringPolicy() { + } + + @Override + public void initialize(RollingFileManager manager) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isTriggeringEvent(LogEvent logEvent) { + throw new UnsupportedOperationException(); + } + + @PluginBuilderFactory + public static SpringBootTriggeringPolicyBuilder newBuilder() { + return new SpringBootTriggeringPolicyBuilder(); + } + + /** + * Builder for creating a {@link TriggeringPolicy}. + */ + public static class SpringBootTriggeringPolicyBuilder implements Builder { + + private static final String DEFAULT_STRATEGY = "size"; + + private static final String DEFAULT_MAX_FILE_SIZE = "10MB"; + + private static final String DEFAULT_TIME_INTERVAL = "1"; + + private static final String DEFAULT_CRON_EXPRESSION = "0 0 0 * * ?"; + + @PluginAttribute("strategy") + private @Nullable String strategy; + + @PluginAttribute("maxFileSize") + private @Nullable String maxFileSize; + + @PluginAttribute("timeInterval") + private @Nullable Integer timeInterval; + + @PluginAttribute("timeModulate") + private @Nullable Boolean timeModulate; + + @PluginAttribute("cronExpression") + private @Nullable String cronExpression; + + @PluginConfiguration + private @Nullable Configuration configuration; + + @Override + public TriggeringPolicy build() { + RollingPolicyStrategy resolvedStrategy = getRollingPolicyStrategy(); + return switch (resolvedStrategy) { + case TIME -> createTimePolicy(); + case SIZE_AND_TIME -> CompositeTriggeringPolicy.createPolicy(createSizePolicy(), createTimePolicy()); + case CRON -> createCronPolicy(); + case SIZE -> createSizePolicy(); + }; + } + + private static RollingPolicyStrategy getRollingPolicyStrategy() { + String resolvedStrategy = getSystemProperty(RollingPolicySystemProperty.STRATEGY, DEFAULT_STRATEGY); + try { + return Objects.requireNonNull(ApplicationConversionService.getSharedInstance() + .convert(resolvedStrategy, RollingPolicyStrategy.class)); + } + catch (ConversionException ex) { + throw new IllegalArgumentException( + "Unsupported rolling policy strategy '%s'".formatted(resolvedStrategy)); + } + } + + private TriggeringPolicy createSizePolicy() { + String size = getSystemProperty(RollingPolicySystemProperty.MAX_FILE_SIZE, DEFAULT_MAX_FILE_SIZE); + return SizeBasedTriggeringPolicy.createPolicy(size); + } + + private TriggeringPolicy createTimePolicy() { + int interval = Integer + .parseInt(getSystemProperty(RollingPolicySystemProperty.TIME_INTERVAL, DEFAULT_TIME_INTERVAL)); + boolean modulate = Boolean + .parseBoolean(getSystemProperty(RollingPolicySystemProperty.TIME_MODULATE, Boolean.FALSE.toString())); + return TimeBasedTriggeringPolicy.newBuilder().setInterval(interval).setModulate(modulate).build(); + } + + private TriggeringPolicy createCronPolicy() { + Assert.notNull(this.configuration, "configuration must not be null"); + Configuration configuration = this.configuration; + + String schedule = getSystemProperty(RollingPolicySystemProperty.CRON, DEFAULT_CRON_EXPRESSION); + return CronTriggeringPolicy.createPolicy(configuration, null, schedule); + } + + private static String getSystemProperty(RollingPolicySystemProperty property, String fallback) { + String value = System.getProperty(property.getEnvironmentVariableName()); + return (value != null) ? value : fallback; + } + + SpringBootTriggeringPolicyBuilder setConfiguration(Configuration configuration) { + this.configuration = configuration; + return this; + } + + } + +} diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java index 5a9bc4152398..4a16f7784b93 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/ColorConverter.java @@ -16,9 +16,11 @@ package org.springframework.boot.logging.logback; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; @@ -26,6 +28,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.pattern.CompositeConverter; +import org.springframework.boot.ansi.AnsiBackground; import org.springframework.boot.ansi.AnsiColor; import org.springframework.boot.ansi.AnsiElement; import org.springframework.boot.ansi.AnsiOutput; @@ -33,8 +36,11 @@ /** * Logback {@link CompositeConverter} to color output using the {@link AnsiOutput} class. - * A single 'color' option can be provided to the converter, or if not specified color - * will be picked based on the logging level. + * One or more styling options can be provided to the converter, or if not specified color + * will be picked based on the logging level. Supported options include foreground colors + * (e.g. {@code red}, {@code bright_blue}), background colors (e.g. {@code bg_red}, + * {@code bg_bright_green}), and text styles (e.g. {@code bold}, {@code underline}, + * {@code reverse}). * * @author Phillip Webb * @since 1.0.0 @@ -48,7 +54,11 @@ public class ColorConverter extends CompositeConverter { Arrays.stream(AnsiColor.values()) .filter((color) -> color != AnsiColor.DEFAULT) .forEach((color) -> ansiElements.put(color.name().toLowerCase(Locale.ROOT), color)); - ansiElements.put("faint", AnsiStyle.FAINT); + Arrays.stream(AnsiStyle.values()) + .forEach((style) -> ansiElements.put(style.name().toLowerCase(Locale.ROOT), style)); + Arrays.stream(AnsiBackground.values()) + .filter((bg) -> bg != AnsiBackground.DEFAULT) + .forEach((bg) -> ansiElements.put("bg_" + bg.name().toLowerCase(Locale.ROOT), bg)); ELEMENTS = Collections.unmodifiableMap(ansiElements); } @@ -63,19 +73,38 @@ public class ColorConverter extends CompositeConverter { @Override protected String transform(ILoggingEvent event, String in) { - AnsiElement color = ELEMENTS.get(getFirstOption()); - if (color == null) { + List options = getOptionList(); + List elements = new ArrayList<>(); + if (options != null) { + for (String option : options) { + String[] optionParts = option.split(","); + for (String optionPart : optionParts) { + AnsiElement element = ELEMENTS.get(optionPart.trim().toLowerCase(Locale.ROOT)); + if (element != null) { + elements.add(element); + } + } + } + } + if (elements.isEmpty()) { // Assume highlighting - color = LEVELS.get(event.getLevel().toInteger()); - color = (color != null) ? color : AnsiColor.GREEN; + AnsiElement element = LEVELS.get(event.getLevel().toInteger()); + elements.add((element != null) ? element : AnsiColor.GREEN); } - return toAnsiString(in, color); + return toAnsiString(in, elements.toArray(new AnsiElement[0])); } protected String toAnsiString(String in, AnsiElement element) { return AnsiOutput.toString(element, in); } + protected String toAnsiString(String in, AnsiElement... elements) { + Object[] ansiParams = new Object[elements.length + 1]; + System.arraycopy(elements, 0, ansiParams, 0, elements.length); + ansiParams[elements.length] = in; + return AnsiOutput.toString(ansiParams); + } + static String getName(AnsiElement element) { return ELEMENTS.entrySet() .stream() diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java index 8ff938aa4d7f..42ed41c7b634 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystemProperties.java @@ -106,7 +106,6 @@ private void applyRollingPolicy(RollingPolicySystemProperty property, PropertyRe private void applyRollingPolicy(RollingPolicySystemProperty property, PropertyResolver resolver, Class type) { T value = getProperty(resolver, property.getApplicationPropertyName(), type); - value = (value != null) ? value : getProperty(resolver, property.getDeprecatedApplicationPropertyName(), type); if (value != null) { String stringValue = String.valueOf((value instanceof DataSize dataSize) ? dataSize.toBytes() : value); setSystemProperty(property.getEnvironmentVariableName(), stringValue); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java index ed823a4ef1d4..9ac424e4faf3 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/RollingPolicySystemProperty.java @@ -29,38 +29,35 @@ public enum RollingPolicySystemProperty { /** * Logging system property for the rolled-over log file name pattern. */ - FILE_NAME_PATTERN("file-name-pattern", "logging.pattern.rolling-file-name"), + FILE_NAME_PATTERN("file-name-pattern"), /** * Logging system property for the clean history on start flag. */ - CLEAN_HISTORY_ON_START("clean-history-on-start", "logging.file.clean-history-on-start"), + CLEAN_HISTORY_ON_START("clean-history-on-start"), /** * Logging system property for the file log max size. */ - MAX_FILE_SIZE("max-file-size", "logging.file.max-size"), + MAX_FILE_SIZE("max-file-size"), /** * Logging system property for the file total size cap. */ - TOTAL_SIZE_CAP("total-size-cap", "logging.file.total-size-cap"), + TOTAL_SIZE_CAP("total-size-cap"), /** * Logging system property for the file log max history. */ - MAX_HISTORY("max-history", "logging.file.max-history"); + MAX_HISTORY("max-history"); private final String environmentVariableName; private final String applicationPropertyName; - private final String deprecatedApplicationPropertyName; - - RollingPolicySystemProperty(String applicationPropertyName, String deprecatedApplicationPropertyName) { + RollingPolicySystemProperty(String applicationPropertyName) { this.environmentVariableName = "LOGBACK_ROLLINGPOLICY_" + name(); this.applicationPropertyName = "logging.logback.rollingpolicy." + applicationPropertyName; - this.deprecatedApplicationPropertyName = deprecatedApplicationPropertyName; } /** @@ -75,8 +72,4 @@ String getApplicationPropertyName() { return this.applicationPropertyName; } - String getDeprecatedApplicationPropertyName() { - return this.deprecatedApplicationPropertyName; - } - } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java index ac1d5cc0bd3c..570c09db4bec 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/logback/SpringBootJoranConfigurator.java @@ -442,7 +442,10 @@ public void acceptWithException(FileHandler file) throws Exception { if (file.exists()) { InputStreamSource content = file.getContent(); Assert.state(content != null, "Unable to get file content"); - byte[] existingContent = content.getInputStream().readAllBytes(); + byte[] existingContent; + try (InputStream inputStream = content.getInputStream()) { + existingContent = inputStream.readAllBytes(); + } if (!Arrays.equals(this.newContent, existingContent)) { throw new IllegalStateException( "Logging configuration differs from the configuration that has already been written. " diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java index 75c268d72017..d5aebb1c10f9 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java @@ -193,24 +193,37 @@ void nested(T item, BiConsumer pairs) { LinkedHashMap result = new LinkedHashMap<>(); this.addedPairs.forEach((addedPair) -> { addedPair.accept(item, joining((name, value) -> { - List nameParts = List.of(name.split("\\.")); + StringBuilder part = new StringBuilder(name.length()); + int length = (!name.endsWith(".")) ? name.length() : name.length() - 1; Map destination = result; - for (int i = 0; i < nameParts.size() - 1; i++) { - Object existing = destination.computeIfAbsent(nameParts.get(i), (key) -> new LinkedHashMap<>()); - if (!(existing instanceof Map)) { - String common = String.join(".", nameParts.subList(0, i + 1)); - throw new IllegalStateException( - "Duplicate nested pairs added under '%s'".formatted(common)); + for (int i = 0; i < length; i++) { + char ch = name.charAt(i); + if (i == length - 1) { + part.append(ch); + Object previous = destination.put(part.toString(), value); + assertNotDuplicateNestedPairs(previous == null, name, length); + } + else if (ch == '.') { + Object current = destination.computeIfAbsent(part.toString(), + (key) -> new LinkedHashMap<>()); + assertNotDuplicateNestedPairs(current instanceof Map, name, i); + destination = (Map) current; + part.setLength(0); + } + else { + part.append(ch); } - destination = (Map) existing; } - Object previous = destination.put(nameParts.get(nameParts.size() - 1), value); - Assert.state(previous == null, () -> "Duplicate nested pairs added under '%s'".formatted(name)); })); }); result.forEach(pairs); } + private void assertNotDuplicateNestedPairs(boolean expression, String name, int index) { + Assert.state(expression, + () -> "Duplicate nested pairs added under '%s'".formatted(name.substring(0, index))); + } + private BiConsumer joining(BiConsumer pairs) { return (name, value) -> { name = this.joiner.join(ContextPairs.this.prefix, (name != null) ? name : ""); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java b/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java index 6b406e2850c1..80f6d7320bb4 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/retry/RetryPolicySettings.java @@ -41,6 +41,12 @@ public final class RetryPolicySettings { */ public static final long DEFAULT_MAX_RETRIES = RetryPolicy.Builder.DEFAULT_MAX_RETRIES; + /** + * Default maximum elapsed time (infinite). + * @since 4.2.0 + */ + public static final Duration DEFAULT_TIMEOUT = RetryPolicy.Builder.DEFAULT_TIMEOUT; + /** * Default initial delay. */ @@ -64,6 +70,8 @@ public final class RetryPolicySettings { private Long maxRetries = DEFAULT_MAX_RETRIES; + private Duration timeout = DEFAULT_TIMEOUT; + private Duration delay = DEFAULT_DELAY; private @Nullable Duration jitter; @@ -85,6 +93,7 @@ public RetryPolicy createRetryPolicy() { map.from(this::getExceptionExcludes).to(builder::excludes); map.from(this::getExceptionPredicate).to(builder::predicate); map.from(this::getMaxRetries).to(builder::maxRetries); + map.from(this::getTimeout).to(builder::timeout); map.from(this::getDelay).to(builder::delay); map.from(this::getJitter).to(builder::jitter); map.from(this::getMultiplier).to(builder::multiplier); @@ -168,6 +177,31 @@ public void setMaxRetries(Long maxRetries) { this.maxRetries = maxRetries; } + /** + * Return the timeout for the maximum amount of elapsed time allowed for the initial + * invocation and any subsequent retry attempts, including delays. + * @return the maximum duration the request can take + * @since 4.2.0 + */ + public Duration getTimeout() { + return this.timeout; + } + + /** + * Specify a timeout for the maximum amount of elapsed time allowed for the initial + * invocation and any subsequent retry attempts, including delays. + *

+ * The default is {@link Duration#ZERO}, which signals that no timeout should be + * applied. + * @param timeout the timeout, typically in milliseconds or seconds; must be greater + * than or equal to zero + * @since 4.2.0 + * @see #DEFAULT_TIMEOUT + */ + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + /** * Return the base delay after the initial invocation. * @return the base delay diff --git a/core/spring-boot/src/main/java/org/springframework/boot/support/SpringFactoriesEnvironmentPostProcessorsFactory.java b/core/spring-boot/src/main/java/org/springframework/boot/support/SpringFactoriesEnvironmentPostProcessorsFactory.java index 745686055cf5..f0bc73105407 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/support/SpringFactoriesEnvironmentPostProcessorsFactory.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/support/SpringFactoriesEnvironmentPostProcessorsFactory.java @@ -16,18 +16,13 @@ package org.springframework.boot.support; -import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import org.springframework.boot.EnvironmentPostProcessor; -import org.springframework.boot.SpringApplication; import org.springframework.boot.bootstrap.BootstrapContext; import org.springframework.boot.bootstrap.BootstrapRegistry; import org.springframework.boot.bootstrap.ConfigurableBootstrapContext; import org.springframework.boot.logging.DeferredLogFactory; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.io.support.SpringFactoriesLoader.ArgumentResolver; @@ -51,35 +46,7 @@ public List getEnvironmentPostProcessors(DeferredLogFa argumentResolver = argumentResolver.and(ConfigurableBootstrapContext.class, bootstrapContext); argumentResolver = argumentResolver.and(BootstrapContext.class, bootstrapContext); argumentResolver = argumentResolver.and(BootstrapRegistry.class, bootstrapContext); - List postProcessors = new ArrayList<>(); - postProcessors.addAll(this.loader.load(EnvironmentPostProcessor.class, argumentResolver)); - postProcessors.addAll(loadDeprecatedPostProcessors(argumentResolver)); - AnnotationAwareOrderComparator.sort(postProcessors); - return postProcessors.stream().map(Adapter::apply).collect(Collectors.toCollection(ArrayList::new)); - } - - @SuppressWarnings("removal") - private List loadDeprecatedPostProcessors( - ArgumentResolver argumentResolver) { - return this.loader.load(org.springframework.boot.env.EnvironmentPostProcessor.class, argumentResolver); - } - - @SuppressWarnings("removal") - record Adapter( - org.springframework.boot.env.EnvironmentPostProcessor postProcessor) implements EnvironmentPostProcessor { - - @Override - public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { - this.postProcessor.postProcessEnvironment(environment, application); - } - - static EnvironmentPostProcessor apply(Object source) { - if (source instanceof EnvironmentPostProcessor environmentPostProcessor) { - return environmentPostProcessor; - } - return new Adapter((org.springframework.boot.env.EnvironmentPostProcessor) source); - } - + return this.loader.load(EnvironmentPostProcessor.class, argumentResolver); } } diff --git a/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationHome.java b/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationHome.java index a8b1ecc3d505..1c968e7149be 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationHome.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationHome.java @@ -23,6 +23,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; +import java.nio.file.Path; import java.security.CodeSource; import java.security.ProtectionDomain; import java.util.Enumeration; @@ -163,6 +164,17 @@ private File findDefaultHomeDir() { return this.source; } + /** + * Returns the path of the underlying source used to find the home directory. This is + * usually the jar file or a directory. Can return {@code null} if the source cannot + * be determined. + * @return the underlying source path or {@code null} + * @since 4.2.0 + */ + public @Nullable Path getSourcePath() { + return (this.source != null) ? this.source.toPath() : null; + } + /** * Returns the application home directory. * @return the home directory (never {@code null}) @@ -171,6 +183,15 @@ public File getDir() { return this.dir; } + /** + * Returns the path of the application home directory. + * @return the home path (never {@code null}) + * @since 4.2.0 + */ + public Path getPath() { + return this.dir.toPath(); + } + @Override public String toString() { return getDir().toString(); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java b/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java index c6395a554b61..42d59d6fba10 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/system/ApplicationTemp.java @@ -117,7 +117,12 @@ public File getDir(String subDir) { return createDirectory(getPath().resolve(subDir)).toFile(); } - private Path getPath() { + /** + * Return the path to be used for application specific temp files. + * @return the application temp path + * @since 4.2.0 + */ + public Path getPath() { if (this.path == null) { this.pathLock.lock(); try { diff --git a/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index cee597c0dfe5..27bc33005af8 100644 --- a/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/core/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -55,10 +55,8 @@ { "name": "logging.file.clean-history-on-start", "type": "java.lang.Boolean", - "description": "Whether to clean the archive log files on startup. Only supported with the default logback setup.", - "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", - "defaultValue": false, "deprecation": { + "level": "error", "replacement": "logging.logback.rollingpolicy.clean-history-on-start", "since": "2.4.0" } @@ -66,10 +64,8 @@ { "name": "logging.file.max-history", "type": "java.lang.Integer", - "description": "Maximum number of archive log files to keep. Only supported with the default logback setup.", - "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", - "defaultValue": 7, "deprecation": { + "level": "error", "replacement": "logging.logback.rollingpolicy.max-history", "since": "2.4.0" } @@ -77,10 +73,8 @@ { "name": "logging.file.max-size", "type": "org.springframework.util.unit.DataSize", - "description": "Maximum log file size. Only supported with the default logback setup.", - "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", - "defaultValue": "10MB", "deprecation": { + "level": "error", "replacement": "logging.logback.rollingpolicy.max-file-size", "since": "2.4.0" } @@ -100,10 +94,8 @@ { "name": "logging.file.total-size-cap", "type": "org.springframework.util.unit.DataSize", - "description": "Total size of log backups to be kept. Only supported with the default logback setup.", - "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", - "defaultValue": "0B", "deprecation": { + "level": "error", "replacement": "logging.logback.rollingpolicy.total-size-cap", "since": "2.4.0" } @@ -139,6 +131,55 @@ "type": "java.util.List", "description": "Overriding configuration files used to create a composite configuration. Can be prefixed with 'optional:' to only load the override if it exists." }, + { + "name": "logging.log4j2.rollingpolicy.cron", + "type": "java.lang.String", + "description": "Cron expression used when the strategy is 'cron'.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "0 0 0 * * ?" + }, + { + "name": "logging.log4j2.rollingpolicy.file-name-pattern", + "type": "java.lang.String", + "description": "Pattern for rolled-over log file names.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz" + }, + { + "name": "logging.log4j2.rollingpolicy.max-file-size", + "type": "org.springframework.util.unit.DataSize", + "description": "Maximum log file size.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "10MB" + }, + { + "name": "logging.log4j2.rollingpolicy.max-history", + "type": "java.lang.Integer", + "description": "Maximum number of archive log files to keep.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": 7 + }, + { + "name": "logging.log4j2.rollingpolicy.strategy", + "type": "org.springframework.boot.logging.log4j2.RollingPolicyStrategy", + "description": "Rolling policy strategy.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": "size" + }, + { + "name": "logging.log4j2.rollingpolicy.time-interval", + "type": "java.lang.Integer", + "description": "Time based triggering interval when the strategy is 'time' or 'size-and-time'.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": 1 + }, + { + "name": "logging.log4j2.rollingpolicy.time-modulate", + "type": "java.lang.Boolean", + "description": "Whether to align the next rollover time to occur at the top of the interval when the strategy is time based.", + "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", + "defaultValue": false + }, { "name": "logging.logback.rollingpolicy.clean-history-on-start", "type": "java.lang.Boolean", @@ -219,10 +260,8 @@ { "name": "logging.pattern.rolling-file-name", "type": "java.lang.String", - "description": "Pattern for rolled-over log file names. Supported only with the default Logback setup.", - "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", - "defaultValue": "${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz", "deprecation": { + "level": "error", "replacement": "logging.logback.rollingpolicy.file-name-pattern", "since": "2.4.0" } diff --git a/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml b/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml index c81eca81ddd0..4762e6e7cbb3 100644 --- a/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml +++ b/core/spring-boot/src/main/resources/org/springframework/boot/logging/log4j2/log4j2-file.xml @@ -6,6 +6,7 @@ yyyy-MM-dd'T'HH:mm:ss.SSSXXX %clr{%d{${sys:LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${sys:LOG_LEVEL_PATTERN}} %clr{%pid}{magenta} %clr{--- %esb{${sys:APPLICATION_NAME:-}}%esb{${sys:APPLICATION_GROUP:-}}[%15.15t] ${sys:LOG_CORRELATION_PATTERN:-}}{faint}%clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} %d{${sys:LOG_DATEFORMAT_PATTERN}} ${sys:LOG_LEVEL_PATTERN} %pid --- %esb{${sys:APPLICATION_NAME:-}}%esb{${sys:APPLICATION_GROUP:-}}[%t] ${sys:LOG_CORRELATION_PATTERN:-}%-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD} + @@ -21,7 +22,7 @@ - +